TRTC Web SDKAPI ReferenceEventsError CodesTypesTutorialsChangelog

Tutorial: Screen Sharing

Screen Sharing

Function Description

This article mainly introduces how to implement screen sharing function in TRTC Web SDK.

Run Demo

Click to Run Demo

Implementation Process

  1. Start Local Screen Sharing

    const trtcA = TRTC.create();
    await trtcA.enterRoom({
      scene: 'rtc',
      sdkAppId: 140000000, // Fill in your sdkAppId
      userId: 'userA', // Fill in your userId
      userSig: 'userA_sig', // Fill in userSig corresponding to userId
      roomId: 6969
    })
    await trtcA.startScreenShare();
    
  2. Play Remote Screen Sharing

    const trtcB = TRTC.create();
    trtcB.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, ({ userId, streamType }) => {
      // Main video stream, generally the stream pushed by the camera
      if (streamType === TRTC.TYPE.STREAM_TYPE_MAIN) {
        // 1. Place a div tag with an id of `${userId}_main` on the page to play the main stream in the div tag. The business side can customize the id of the div tag. This is just an example.
        // 2. Play the main video stream
        trtcB.startRemoteVideo({ userId, streamType,  view: `${userId}_main` });
      } else {
        // Sub video stream, generally the stream pushed by screen sharing.
        // 1. Place a div tag with an id of `${userId}_screen` on the page to play the screen sharing in the div tag. The business side can customize the id of the div tag. This is just an example.
        // 2. Play screen sharing
        trtcB.startRemoteVideo({ userId, streamType, view: `${userId}_screen` });
      }
    });
    await trtcB.enterRoom({
      scene: 'rtc',
      sdkAppId: 140000000, // Fill in your sdkAppId
      userId: 'userB', // Fill in your userId
      userSig: 'userB_sig', // Fill in userSig corresponding to userId
      roomId: 6969
    })
    
  3. Start Camera + Screen Sharing at the Same Time

     await trtcA.startLocalVideo();
     await trtcA.startScreenShare();
    
  4. Screen Sharing + System Audio

    System audio capture:

    • Chrome M74+ supports capturing tab audio.
    • Capturing system audio (entire OS sound):
      • Windows / Chrome OS: Supported.
      • macOS: Requires Chrome 141+ and macOS 14.2+.
      • Linux: Not supported.
    • Other browsers (Firefox, Safari, etc.) are not supported.
     await trtcA.startScreenShare({ option: { systemAudio: true }});
    

    Check Share audio in the pop-up dialog box, and the system audio will be mixed with the local microphone and published. Other users in the room will receive the TRTC.EVENT.REMOTE_AUDIO_AVALIABLE event.

  5. Stop Screen Sharing

     // Stop screen sharing collection and publishing
     await trtcA.stopScreenShare();
     // Other users in the room will receive the TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE event, and streamType is TRTC.TYPE.STREAM_TYPE_SUB.
     trtcB.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, ({ userId, streamType }) => {
        if (streamType === TRTC.TYPE.STREAM_TYPE_SUB) {
        }
     })
    

    In addition, users may also stop screen sharing through the browser's own button, so the screen sharing stream needs to listen for the screen sharing stop event and respond accordingly.

    // Listen for screen sharing stop event
    trtcA.on(TRTC.EVENT.SCREEN_SHARE_STOPPED, () => {
      console.log('screen sharing was stopped');
    });
    

Screen sharing in Electron

Option 1: Use getDisplayMedia directly (Electron 22+, recommended)

Starting from Electron 22, Electron supports the WebRTC standard getDisplayMedia interface. Unlike Chrome browser, Electron requires the following configuration in the main process. Once configured, the renderer can call trtc.startScreenShare() just like in a browser.

1. Main process configuration

// main.js
const { app, BrowserWindow, session, desktopCapturer } = require('electron');
// macOS: Enable system audio loopback (must be set before app.whenReady())
if (process.platform === 'darwin') {
  app.commandLine.appendSwitch('enable-features', 'MacLoopbackAudioForScreenShare,MacSckSystemAudioLoopbackOverride');
}
app.whenReady().then(async () => {
  // ① Grant media (camera/microphone) and display-capture (screen share) permissions
  session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
    callback(['media', 'display-capture'].includes(permission));
  });
  // ② Register getDisplayMedia request handler to provide screen share sources
  //    Returning audio: 'loopback' also captures system audio (see "System audio" below)
  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 {
        // Show a custom picker window when multiple sources exist
        showSourcePicker(sources, callback);
      }
    }).catch(() => callback({}));
  });
});
// Show a custom picker window when multiple screen/window sources exist
function showSourcePicker(sources, callback) {
  const { ipcMain } = require('electron');
  const pickerWin = new BrowserWindow({
    width: 800, height: 600, modal: true, title: 'Select what to share',
    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>Select what to share</h2>${html}`);
}

2. Call startScreenShare() directly in the renderer process

await trtc.startScreenShare();

Notes:

  • Screen recording permission cannot be requested programmatically. You must manually grant it in System Settings > Privacy & Security > Screen Recording for "Electron" (the name shown in dev mode), then fully quit and restart the app for it to take effect.
  • On macOS, capturing a Keynote fullscreen presentation may not automatically switch to the presentation window (known issue: Electron issue #37126).

System audio capture

Return audio: 'loopback' in the setDisplayMediaRequestHandler callback to capture system audio (feature flags are already configured in the main code above). Platform support:

Platform System Audio Notes
Windows / Linux Natively supported
macOS 13+ Requires Chromium feature flags (configured in the main code above)
macOS 12.7.6 and below Use a virtual audio device like BlackHole

Option 2: Custom capture with desktopCapturer (Electron below 22 or when a specific source is needed)

If your Electron version is below 22, or you need to programmatically enumerate and specify which window/screen to capture, you can use Electron's desktopCapturer.getSources({ types: ['window', 'screen'] }) API for custom capture:

  1. The main process main.js listens for the page to finish loading, gets the list of screen share sources via desktopCapturer.getSources, and sends the list of screen share sources to the rendering process.
  2. The rendering process listens to the main process events to get a list of screen share sources.
  3. Create screen share streams and push them through the TRTC Web SDK.
    1. Get the screen-sharing MediaStream from the system API via navigator.mediaDevices.getUserMedia().
    2. Pass the custom captured videoTrack in the option parameter of TRTC.startScreenShare() to push the screen sharing stream.
// 【1】main.js Master process gets list of screen share sources
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】Render process listens to main process events to get a list of screen share sources
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】Rendering process push screen share
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, // screen share source 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)
  }
}

Precautions

  1. What is the main/sub video stream?
  2. The SDK uses the 1080p parameter configuration by default to collect screen sharing. For details, refer to the interface: startScreenShare

Common Issues

  1. Safari screen sharing error getDisplayMedia must be called from a user gesture handler

    This is because Safari restricts the getDisplayMedia screen capture interface, which must be called within 1 second of the callback function of the user click event.

    Reference: webkit issue.

    // good
    async function onClick() {
      // It is recommended to execute the collection logic first when onClick is executed
      await trtcA.startScreenShare();
      await trtcA.enterRoom({ 
        roomId: 123123,
        sdkAppId: 140000000, // Fill in your sdkAppId
        userId: 'userA', // Fill in your userId
        userSig: 'userA_sig', // Fill in userSig corresponding to userId
      });
    }
    // bad
    async function onClick() {
      await trtcA.enterRoom({ 
        roomId: 123123,
        sdkAppId: 140000000, // Fill in your sdkAppId
        userId: 'userA', // Fill in your userId
        userSig: 'userA_sig', // Fill in userSig corresponding to userId
      });
      // Entering the room may take more than 1s, and the collection may fail
      await trtcA.startScreenShare();
    }
    
  2. macOS Monterey(12.2.1), device permissions need to be requested in the master process

    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 screen sharing fails with the error message "NotAllowedError: Permission denied by system" or "NotReadableError: Could not start video source" when screen recording is already authorized. Chrome bug. Solution: Open 【Settings】> Click 【Security & Privacy】> Click 【Privacy】> Click 【Screen Recording】> Turn off Chrome screen recording authorization > Reopen Chrome screen recording authorization > Close Chrome browser > Reopen Chrome browser.

  4. WebRTC screen sharing known issues and workarounds