Chrome Extension Webrtc Screen Sharing — Best Practices

7 min read

WebRTC Screen Sharing Patterns for Chrome Extensions

This guide covers WebRTC-based screen and tab capture patterns for Chrome extensions, including desktop capture, tab capture, and streaming implementations.

Prerequisites

Declare the required permissions in your manifest:

{
  "manifest_version": 3,
  "permissions": ["desktopCapture", "tabCapture"],
  "host_permissions": ["<all_urls>"]
}

chrome.desktopCapture.getMediaStreamId()

Get a stream ID for tab, screen, or window capture:

// utils/capture.ts
// chrome.desktopCapture.chooseDesktopMedia() shows a picker and returns a stream ID.
// It must be called from a user gesture context (e.g., action click).
function chooseDesktopMedia(tabToShareWith: chrome.tabs.Tab): Promise<string> {
  return new Promise((resolve, reject) => {
    chrome.desktopCapture.chooseDesktopMedia(
      ['screen', 'window', 'tab'],
      tabToShareWith,
      (streamId) => {
        if (streamId) resolve(streamId);
        else reject(new Error('User cancelled'));
      }
    );
  });
}

// Use stream ID with navigator.mediaDevices.getUserMedia()
async function startCapture(streamId: string): Promise<MediaStream> {
  return navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      // @ts-ignore - Chrome-specific constraint
      mandatory: {
        chromeMediaSource: 'desktop',
        chromeMediaSourceId: streamId
      }
    }
  });
}

chrome.tabCapture.capture()

Capture a specific tab’s audio and video:

// utils/tabCapture.ts
// In MV3, use chrome.tabCapture.getMediaStreamId() to get a stream ID,
// then use navigator.mediaDevices.getUserMedia() with the stream ID.
async function captureTab(tabId: number): Promise<MediaStream> {
  const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tabId });
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
      mandatory: {
        chromeMediaSource: 'tab',
        chromeMediaSourceId: streamId
      }
    } as any,
    video: {
      mandatory: {
        chromeMediaSource: 'tab',
        chromeMediaSourceId: streamId,
        minWidth: 1280,
        minHeight: 720,
        maxWidth: 1920,
        maxHeight: 1080
      }
    } as any
  });
  return stream;
}

getDisplayMedia in Extension Pages

Use getDisplayMedia in extension contexts (popup, options, side panel):

// popup/main.ts
async function startScreenShare(): Promise<MediaStream | null> {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        displaySurface: 'monitor',
        width: { ideal: 1920 },
        height: { ideal: 1080 }
      },
      audio: true
    });
    
    // Handle user stopping via browser UI
    stream.getVideoTracks()[0].onended = () => {
      console.log('Screen share ended by user');
    };
    
    return stream;
  } catch (err) {
    console.error('Screen share cancelled:', err);
    return null;
  }
}

MediaRecorder for Recording

Record captured streams locally:

// utils/recorder.ts
class StreamRecorder {
  private mediaRecorder: MediaRecorder | null = null;
  private chunks: Blob[] = [];

  startRecording(stream: MediaStream, mimeType: string = 'video/webm;codecs=vp9'): void {
    this.chunks = [];
    this.mediaRecorder = new MediaRecorder(stream, { mimeType });
    
    this.mediaRecorder.ondataavailable = (e) => {
      if (e.data.size > 0) this.chunks.push(e.data);
    };
    
    this.mediaRecorder.start(1000); // Capture in 1-second chunks
  }

  async stopRecording(): Promise<Blob> {
    return new Promise((resolve) => {
      this.mediaRecorder.onstop = () => {
        resolve(new Blob(this.chunks, { type: 'video/webm' }));
      };
      this.mediaRecorder.stop();
    });
  }
}

Streaming via WebRTC Peer Connections

Stream captured content to a remote peer:

// utils/webrtc-stream.ts
async function streamToPeer(
  captureStream: MediaStream, 
  peerConnection: RTCPeerConnection
): Promise<MediaStreamTrack[]> {
  captureStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, captureStream);
  });
  
  const answer = await peerConnection.createAnswer();
  await peerConnection.setLocalDescription(answer);
  return peerConnection.getSenders().map(s => s.track);
}

Canvas Frame Processing

Process video frames using canvas:

// utils/canvas-processor.ts
class FrameProcessor {
  private video: HTMLVideoElement;
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;

  constructor() {
    this.video = document.createElement('video');
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
  }

  processFrame(stream: MediaStream): ImageData {
    this.video.srcObject = stream;
    this.canvas.width = this.video.videoWidth;
    this.canvas.height = this.video.videoHeight;
    this.ctx.drawImage(this.video, 0, 0);
    return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
  }

  captureScreenshot(stream: MediaStream): string {
    this.processFrame(stream);
    return this.canvas.toDataURL('image/png');
  }
}

MV3 Considerations

Capture APIs require extension page context. Use offscreen documents for background capture:

// background/offscreen.ts
async function createCaptureOffscreen(): Promise<void> {
  const existingContexts = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT']
  });
  
  if (existingContexts.length === 0) {
    await chrome.offscreen.createDocument({
      url: 'offscreen-capture.html',
      reasons: ['USER_MEDIA'],
      justification: 'Background tab capture for streaming'
    });
  }
}

Privacy Best Practices


Cross-References

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.