TRTC Web SDKAPI 文档事件错误码类型教程更新日志
En

Tutorial: 屏幕分享

屏幕分享

功能描述

本文主要介绍如何在 TRTC Web SDK 实现屏幕分享功能。

体验在线 Demo

点击运行 Demo

实现流程

  1. 【推流端】开启屏幕分享

    const trtcA = TRTC.create();
    await trtcA.enterRoom({
      scene: 'rtc',
      sdkAppId: 140000000, // 填写您的 sdkAppId
      userId: 'userA', // 填写您的 userId
      userSig: 'userA_sig', // 填写 userId 对应的 userSig
      roomId: 6969
    })
    await trtcA.startScreenShare();
    
  2. 【拉流端】播放屏幕分享

    const trtcB = TRTC.create();
    trtcB.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, ({ userId, streamType }) => {
      // 主路视频流,一般是推摄像头的那路流
      if (streamType === TRTC.TYPE.STREAM_TYPE_MAIN) {
        // 1. 在页面中放置一个 id 为 `${userId}_main` 的 div 标签,用于在 div 标签内播放主路流。业务侧可自定义 div 标签的 id,此处只是举例说明。
        // 2. 播放主路视频流
        trtcB.startRemoteVideo({ userId, streamType,  view: `${userId}_main` });
      } else {
        // 辅路视频流,一般是推屏幕分享的那路流。
        // 1. 在页面中放置一个 id 为 `${userId}_screen` 的 div 标签,用于在 div 标签内播放屏幕分享。业务侧可自定义 div 标签的 id,此处只是举例说明。
        // 2. 播放屏幕分享
        trtcB.startRemoteVideo({ userId, streamType, view: `${userId}_screen` });
      }
    });
    await trtcB.enterRoom({
      scene: 'rtc',
      sdkAppId: 140000000, // 填写您的 sdkAppId
      userId: 'userB', // 填写您的 userId
      userSig: 'userB_sig', // 填写 userId 对应的 userSig
      roomId: 6969
    })
    
  3. 同时推摄像头 + 屏幕分享

     await trtcA.startLocalVideo();
     await trtcA.startScreenShare();
    
  4. 屏幕分享 + 系统音频

    采集系统音频:

    • Chrome M74+ 开始支持采集标签页音频
    • 采集系统音频(整个系统的声音)
      • Windows / Chrome OS:支持。
      • macOS:需 Chrome 141+ 且 macOS 14.2+。
      • Linux:不支持。
    • 其它浏览器(Firefox、Safari 等)均不支持。
     await trtcA.startScreenShare({ option: { systemAudio: true }});
    

    在弹出的对话框中勾选分享音频,系统音频会与本地麦克风混音后发布,房间内其他用户会收到 TRTC.EVENT.REMOTE_AUDIO_AVALIABLE 事件

  5. 停止屏幕分享

     // 停止屏幕分享采集及发布
     await trtcA.stopScreenShare();
     // 房间内的其他用户会收到 TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE 事件,streamType 是 TRTC.TYPE.STREAM_TYPE_SUB。
     trtcB.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, ({ userId, streamType }) => {
        if (streamType === TRTC.TYPE.STREAM_TYPE_SUB) {
        }
     })
    

    另外用户还可能会通过浏览器自带的按钮停止屏幕分享,因此屏幕分享流需要监听屏幕分享停止事件,并进行相应的处理。

    // 监听屏幕分享停止事件
    trtcA.on(TRTC.EVENT.SCREEN_SHARE_STOPPED, () => {
      console.log('screen sharing was stopped');
    });
    

在 Electron 中屏幕分享

方案一:直接使用 getDisplayMedia(Electron 22+,推荐)

Electron 22 起,Electron 已支持 WebRTC 标准的 getDisplayMedia 接口。与 Chrome 浏览器不同,Electron 需要在主进程中进行如下配置,渲染进程即可像浏览器一样直接调用 trtc.startScreenShare()

1. 主进程配置

// main.js
const { app, BrowserWindow, session, desktopCapturer } = require('electron');
// macOS: 启用系统音频 loopback(需在 app.whenReady() 之前设置)
if (process.platform === 'darwin') {
  app.commandLine.appendSwitch('enable-features', 'MacLoopbackAudioForScreenShare,MacSckSystemAudioLoopbackOverride');
}
app.whenReady().then(async () => {
  // ① 授予 media(摄像头/麦克风)和 display-capture(屏幕分享)权限
  session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
    callback(['media', 'display-capture'].includes(permission));
  });
  // ② 注册 getDisplayMedia 请求处理器,提供屏幕分享采集源
  //    返回 audio: 'loopback' 可同时采集系统音频(详见下方"系统音频"说明)
  session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
    desktopCapturer.getSources({
      types: ['screen', 'window'],
      thumbnailSize: { width: 320, height: 180 },
    }).then((sources) => {
      if (sources.length <= 1) {
        callback({ video: sources[0], audio: 'loopback' });
      } else {
        // 多个源时弹出自定义选择窗口,showSourcePicker 实现见下方
        showSourcePicker(sources, callback);
      }
    }).catch(() => callback({}));
  });
});
// 当存在多个屏幕/窗口源时,弹出自定义选择窗口让用户选择
function showSourcePicker(sources, callback) {
  const { ipcMain } = require('electron');
  const pickerWin = new BrowserWindow({
    width: 800, height: 600, modal: true, title: '选择分享内容',
    webPreferences: { nodeIntegration: true, contextIsolation: false },
  });
  const selectHandler = (event, sourceId) => {
    const source = sources.find(s => s.id === sourceId);
    ipcMain.removeListener('source-selected', selectHandler);
    pickerWin.destroy();
    callback(source ? { video: source, audio: 'loopback' } : {});
  };
  ipcMain.on('source-selected', selectHandler);
  pickerWin.on('closed', () => callback({}));
  const html = sources.map(s => `
    <div onclick="require('electron').ipcRenderer.send('source-selected', '${s.id}')"
         style="cursor:pointer;padding:12px;border:1px solid #ccc;margin:8px;">
      <img src="${s.thumbnail.toDataURL()}" style="width:160px;height:90px;"/>
      <div>${s.name}</div>
    </div>`).join('');
  pickerWin.loadURL(`data:text/html,<h2>选择要分享的内容</h2>${html}`);
}

2. 渲染进程直接调用 startScreenShare()

await trtc.startScreenShare();

注意事项:

  • 屏幕录制权限无法通过代码请求,需在「系统设置 > 隐私与安全性 > 屏幕录制」中手动为 Electron 授权,授权后必须完全退出并重启应用才能生效。dev 模式下应用在列表中显示为 "Electron"。
  • 在 macOS 上采集 Keynote 全屏演示时,可能存在不会自动切换到演示窗口的已知问题(Electron issue #37126)。

系统音频采集

setDisplayMediaRequestHandler 的 callback 中返回 audio: 'loopback' 即可采集系统音频(feature flags 已在上方主代码中配置)。各平台支持情况:

平台 系统音频 说明
Windows / Linux 原生支持
macOS 13+ 需启用 Chromium feature flags(已在上方代码中配置)
macOS 12.7.6 及以下 需借助 BlackHole 等虚拟音频设备

方案二:使用 desktopCapturer 自定义采集(Electron 22 以下或需指定采集源)

如果 Electron 版本低于 22,或者需要在代码中主动枚举并指定采集哪个窗口/屏幕,可以使用 Electron 的 desktopCapturer.getSources({ types: ['window', 'screen'] }) API 自定义采集:

  1. 主进程 main.js 监听页面加载完成后,通过 desktopCapturer.getSources 获取屏幕分享源列表,并将屏幕分享源列表发送给渲染进程
  2. 渲染进程监听主进程事件,从而获取屏幕分享源列表
  3. 创建屏幕分享流并通过 TRTC Web SDK 进行推流
    1. 通过 navigator.mediaDevices.getUserMedia() 从系统 API 获取屏幕分享的 MediaStream
    2. TRTC.startScreenShare()option参数中传入自定义采集的 videoTrack,推屏幕分享流
// 【1】main.js 主进程获取屏幕分享源列表
const { app, BrowserWindow, desktopCapturer, systemPreferences } = require('electron');
function createWindow () {
  const win = new BrowserWindow({
    // ...
  });
  win.loadFile('./src/index.html');
  win.webContents.on("did-finish-load", async () => {
    if (win) {
      const sources = await desktopCapturer.getSources({
        types: ["window", "screen"],
      });
      win.webContents.send("SEND_SCREEN_SHARE_SOURCES", sources);
    }
  });
}
// 【2】渲染进程监听主进程事件拿到屏幕分享源列表
const { ipcRenderer } = require('electron');
let shareSourceList = [];
ipcRenderer.on('SEND_SCREEN_SHARE_SOURCES', async (event, sources) => {
  const selectContainer = window.document.getElementById('screen-share-select');
  shareSourceList = sources;
  sources.forEach(obj => {
    const optionElement = document.createElement('option');
    optionElement.innerText = `${obj.name}`;
    selectContainer.appendChild(optionElement);
  });
})
// 【3】渲染进程推屏幕分享
async function startScreenShare() {
  const selectContainer = document.getElementById('screen-share-select');
  const selectValue = selectContainer.options[selectContainer.selectedIndex].value;
  const [ source ] = shareSourceList.filter(obj => obj.name === `${selectValue}`);
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: source.id, // 屏幕分享源 id
          minWidth: 1280,
          maxWidth: 1280,
          minHeight: 720,
          maxHeight: 720
        }
      }
    });
    const trtc = TRTC.create();
    await trtc.enterRoom({
      // ...
    });
    await trtc.startScreenShare({
      option: {
        videoTrack: stream.getVideoTracks()[0],
      }
    })
  } catch (error) {
    console.error('start screen share error = ', error)
  }
}

注意事项

  1. 什么是主流,辅流?
  2. SDK 默认使用 1080p 参数配置来采集屏幕分享,具体参考接口:startScreenShare

常见问题

  1. Safari 屏幕分享出现报错 getDisplayMedia must be called from a user gesture handler

    这是因为 Safari 限制了 getDisplayMedia 屏幕采集的接口,必须在用户点击事件的回调函数执行的 1 秒内才可以调用。

    参考:webkit issue

    // good
    async function onClick() {
      // 建议在 onClick 执行时,先执行采集逻辑
      await trtcA.startScreenShare();
      await trtcA.enterRoom({ 
        roomId: 123123,
        sdkAppId: 140000000, // 填写您的 sdkAppId
        userId: 'userA', // 填写您的 userId
        userSig: 'userA_sig', // 填写 userId 对应的 userSig });
    });
    // bad
    async function onClick() {
      await trtcA.enterRoom({ 
        roomId: 123123,
        sdkAppId: 140000000, // 填写您的 sdkAppId
        userId: 'userA', // 填写您的 userId
        userSig: 'userA_sig', // 填写 userId 对应的 userSig });
      })
      // 进房可能耗时超过 1s,可能会采集失败
       await trtcA.startScreenShare();
    }
    
  2. macOS Monterey(12.2.1), 在 Electron 环境下使用屏幕分享需在主进程中请求设备权限

    async function checkAndApplyDeviceAccessPrivilege() {
      const cameraPrivilege = systemPreferences.getMediaAccessStatus('camera');
      console.log(
        `checkAndApplyDeviceAccessPrivilege before apply cameraPrivilege: ${cameraPrivilege}`
      );
      if (cameraPrivilege !== 'granted') {
        await systemPreferences.askForMediaAccess('camera');
      }
      const micPrivilege = systemPreferences.getMediaAccessStatus('microphone');
      console.log(
        `checkAndApplyDeviceAccessPrivilege before apply micPrivilege: ${micPrivilege}`
      );
      if (micPrivilege !== 'granted') {
        await systemPreferences.askForMediaAccess('microphone');
      }
      const screenPrivilege = systemPreferences.getMediaAccessStatus('screen');
      console.log(
        `checkAndApplyDeviceAccessPrivilege before apply screenPrivilege: ${screenPrivilege}`
      );
    }
    
  3. Mac Chrome 在已授权屏幕录制的情况下屏幕分享失败,出现 "NotAllowedError: Permission denied by system" 或者 "NotReadableError: Could not start video source" 错误信息,Chrome bug。解决方案:打开【设置】> 点击【安全性与隐私】> 点击【隐私】> 点击【屏幕录制】> 关闭 Chrome 屏幕录制授权 > 重新打开 Chrome 屏幕录制授权 > 关闭 Chrome 浏览器 > 重新打开 Chrome 浏览器。

  4. WebRTC 屏幕分享已知问题及规避方案