Chrome Extension Tab Capture — Developer Guide

17 min read

Chrome Extension Tab Capture API

The Chrome Extension Tab Capture API is a powerful feature that allows extensions to capture the visual and audio content of browser tabs. This API opens up a wide range of possibilities, from building screen recording extensions to creating collaborative whiteboarding tools. In this comprehensive guide, we’ll explore every aspect of the Tab Capture API, from basic usage to advanced implementation patterns.

Overview and Permissions

The chrome.tabCapture API provides the ability to capture the content of a tab as a media stream. Before using this API, you need to declare the appropriate permissions in your extension’s manifest file.

Manifest Permissions

To use the Tab Capture API, you must add the "tabCapture" permission to your manifest:

{
  "name": "Tab Capture Extension",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "tabCapture"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

It’s important to note that the "tabCapture" permission alone doesn’t automatically grant access to all tabs. The user must initiate the capture through a user gesture, such as clicking a button in your extension’s popup or background script.

Understanding Capture Constraints

The Tab Capture API works in conjunction with the Chrome desktopCapture API. When capturing a tab, you can specify various constraints to control what gets captured:

const constraints = {
  audio: true,
  video: {
    mandatory: {
      chromeMediaSource: 'tab',
      chromeMediaSourceId: streamId
    }
  }
};

Capturing Tab Audio and Video

The primary method for capturing a tab is chrome.tabCapture.capture(). This method initiates the capture and returns a MediaStream object that you can use in various ways.

Basic Capture Implementation

Here’s a fundamental example of how to capture a tab:

async function captureTab(tabId) {
  try {
    const stream = await chrome.tabCapture.capture({
      audio: true,
      video: true
    });
    
    console.log('Capture started successfully');
    return stream;
  } catch (error) {
    console.error('Capture failed:', error);
    throw error;
  }
}

Capture Options

The capture() method accepts an options object with the following properties:

const captureOptions = {
  audio: {
    mandatory: {
      chromeMediaSource: 'tab',
      echoCancellation: true,
      noiseSuppression: true
    }
  },
  video: {
    mandatory: {
      chromeMediaSource: 'tab',
      maxWidth: 1920,
      maxHeight: 1080,
      maxFrameRate: 30
    }
  }
};

const stream = await chrome.tabCapture.capture(captureOptions);

MediaStream Handling and Processing

Once you have a MediaStream from tab capture, you can process it in various ways. The stream behaves like any standard MediaStream, allowing you to work with its tracks using the MediaStream API.

Accessing Audio and Video Tracks

function processStream(stream) {
  const audioTracks = stream.getAudioTracks();
  const videoTracks = stream.getVideoTracks();
  
  audioTracks.forEach(track => {
    console.log('Audio track:', track.label);
    // Configure audio processing
    track.enabled = true;
  });
  
  videoTracks.forEach(track => {
    console.log('Video track:', track.label);
    // Configure video processing
    track.enabled = true;
  });
  
  return { audioTracks, videoTracks };
}

Creating Processed Streams

You can use MediaStreamTrackProcessor and MediaStreamTrackGenerator (available in modern browsers) to process and transform captured media:

async function createProcessedStream(sourceStream) {
  const videoTrack = sourceStream.getVideoTracks()[0];
  const audioTrack = sourceStream.getAudioTracks()[0];
  
  // Create a track processor for video
  const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
  const videoGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
  
  // Transform the video (example: add a filter)
  const transformer = new TransformStream({
    transform(videoFrame, controller) {
      // Apply processing to the frame
      controller.enqueue(videoFrame);
    }
  });
  
  videoProcessor.readable.pipeThrough(transformer).pipeTo(videoGenerator.writable);
  
  // Create new stream with processed tracks
  return new MediaStream([videoGenerator, audioTrack]);
}

Recording Captured Content

One of the most common use cases for Tab Capture is recording the tab’s content. Here’s how to implement a basic recorder:

class TabRecorder {
  constructor(stream) {
    this.stream = stream;
    this.mediaRecorder = null;
    this.chunks = [];
  }
  
  startRecording() {
    this.chunks = [];
    
    this.mediaRecorder = new MediaRecorder(this.stream, {
      mimeType: 'video/webm;codecs=vp9'
    });
    
    this.mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        this.chunks.push(event.data);
      }
    };
    
    this.mediaRecorder.start(1000); // Collect data every second
    console.log('Recording started');
  }
  
  stopRecording() {
    return new Promise((resolve) => {
      this.mediaRecorder.onstop = () => {
        const blob = new Blob(this.chunks, { type: 'video/webm' });
        resolve(blob);
      };
      
      this.mediaRecorder.stop();
      console.log('Recording stopped');
    });
  }
  
  downloadRecording(filename = 'recording.webm') {
    return this.stopRecording().then(blob => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
    });
  }
}

Tab Capture Indicators and User Awareness

When a tab is being captured, Chrome displays a visual indicator to inform the user. This is an important UX consideration that you should be aware of when building capture extensions.

Understanding the Recording Indicator

Chrome automatically shows a red recording indicator in the browser’s address bar when a tab is being captured. This indicator:

This is a security feature to ensure transparency with users about when their tab content is being recorded.

Detecting Capture State

You can check if a tab is currently being captured using chrome.tabCapture.getCapturedTabs():

async function getCapturedTabInfo() {
  const tabs = await chrome.tabCapture.getCapturedTabs();
  
  tabs.forEach(tab => {
    console.log(`Tab ${tab.id}: ${tab.status}`);
  });
  
  return tabs;
}

The returned objects contain:

Handling Fullscreen Changes

When a user enters fullscreen mode during capture, you need to handle it properly:

stream.getVideoTracks()[0].onended = () => {
  console.log('Capture ended - possibly due to fullscreen change');
  // Handle the ended event appropriately
};

getMediaStreamId for Offscreen Document Capture

In Manifest V3, service workers have limited lifetime, making continuous capture challenging. The chrome.tabCapture.getMediaStreamId() method provides a solution by generating a stream ID that can be used in various contexts, including offscreen documents.

Generating a Stream ID

async function getStreamId(tabId) {
  const streamId = await chrome.tabCapture.getMediaStreamId({
    targetTabId: tabId
  });
  
  console.log('Stream ID:', streamId);
  return streamId;
}

The getMediaStreamId() method accepts options:

Using Stream ID in Offscreen Documents

Offscreen documents in Manifest V3 provide a way to handle long-running tasks that don’t fit in the service worker lifecycle. Here’s how to use Tab Capture with offscreen documents:

First, create an offscreen document:

async function createOffscreenDocument() {
  const existingContexts = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT']
  });
  
  if (existingContexts.length === 0) {
    await chrome.offscreen.createDocument({
      url: 'offscreen.html',
      reasons: ['USER_INTERACTION'],
      justification: 'Recording tab capture for later download'
    });
  }
}

Then use the stream ID in your offscreen document:

// In offscreen.html/offscreen.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'startCapture') {
    const streamId = message.streamId;
    
    navigator.mediaDevices.getUserMedia({
      video: {
        mandatory: {
          chromeMediaSource: 'tab',
          chromeMediaSourceId: streamId
        }
      }
    }).then(stream => {
      // Process the stream
      sendResponse({ success: true });
    });
    
    return true; // Keep channel open for async response
  }
});

Building a Tab Recording Extension

Now let’s put everything together to build a complete tab recording extension. This example demonstrates best practices and real-world implementation patterns.

// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  document.getElementById('startBtn').addEventListener('click', async () => {
    // Request capture
    const stream = await chrome.tabCapture.capture({
      audio: true,
      video: true
    });
    
    if (stream) {
      // Store stream reference for later use
      chrome.storage.local.set({ 
        captureStream: true,
        tabId: tab.id 
      });
      
      // Notify background script
      chrome.runtime.sendMessage({
        action: 'captureStarted',
        tabId: tab.id
      });
      
      updateUI('recording');
    }
  });
  
  document.getElementById('stopBtn').addEventListener('click', () => {
    chrome.runtime.sendMessage({ action: 'stopCapture' });
    updateUI('stopped');
  });
});

function updateUI(state) {
  const startBtn = document.getElementById('startBtn');
  const stopBtn = document.getElementById('stopBtn');
  
  if (state === 'recording') {
    startBtn.disabled = true;
    stopBtn.disabled = false;
  } else {
    startBtn.disabled = false;
    stopBtn.disabled = true;
  }
}

Background Script Handler

// background.js
let currentRecorder = null;

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'captureStarted') {
    handleCaptureStart(message.tabId);
  } else if (message.action === 'stopCapture') {
    handleCaptureStop();
  }
});

async function handleCaptureStart(tabId) {
  const stream = await chrome.tabCapture.capture({
    audio: true,
    video: true
  });
  
  currentRecorder = new TabRecorder(stream);
  currentRecorder.startRecording();
  
  // Store recorder reference
  chrome.storage.local.set({ 
    recorderActive: true 
  });
}

async function handleCaptureStop() {
  if (currentRecorder) {
    await currentRecorder.downloadRecording(`tab-recording-${Date.now()}.webm`);
    currentRecorder = null;
    
    chrome.storage.local.set({ 
      recorderActive: false 
    });
  }
}

Advanced Patterns and Best Practices

Error Handling

Always implement robust error handling for capture operations:

async function safeCapture(tabId) {
  try {
    // Check if tab exists
    const tab = await chrome.tabs.get(tabId);
    if (!tab.id) {
      throw new Error('Tab not found');
    }
    
    // Attempt capture
    const stream = await chrome.tabCapture.capture({
      audio: true,
      video: true
    });
    
    if (!stream) {
      throw new Error('Capture returned no stream');
    }
    
    // Handle stream errors
    stream.getTracks().forEach(track => {
      track.onended = () => {
        console.log('Track ended:', track.kind);
        handleTrackEnd(track);
      };
      
      track.onmute = () => {
        console.log('Track muted:', track.kind);
      };
      
      track.onunmute = () => {
        console.log('Track unmuted:', track.kind);
      };
    });
    
    return stream;
    
  } catch (error) {
    console.error('Capture error:', error);
    throw error;
  }
}

function handleTrackEnd(track) {
  // Clean up resources
  if (track.kind === 'video') {
    // Handle video track end
  } else if (track.kind === 'audio') {
    // Handle audio track end
  }
}

Performance Optimization

For optimal performance when capturing tabs:

function optimizeCaptureSettings() {
  return {
    video: {
      mandatory: {
        // Request only what's needed
        minWidth: 1280,
        minHeight: 720,
        maxFrameRate: 30, // Reduce for better performance
        // Use efficient codec
        chromeMediaSource: 'tab'
      }
    },
    audio: {
      mandatory: {
        chromeMediaSource: 'tab',
        // Disable echo cancellation if not needed
        echoCancellation: false,
        // Disable noise suppression for better CPU usage
        noiseSuppression: false
      }
    }
  };
}

Security Considerations

When implementing tab capture, keep these security best practices in mind:

  1. Always require user gesture - Never start capture without explicit user action
  2. Validate tab ID - Ensure the tab ID is valid before attempting capture
  3. Clean up resources - Always stop tracks and release resources when done
  4. Handle permissions gracefully - Check if the user has granted necessary permissions
  5. Secure the stream - Don’t share stream IDs across untrusted contexts
async function secureCapture(tabId) {
  // Validate tab exists and is accessible
  try {
    await chrome.tabs.get(tabId);
  } catch (error) {
    throw new Error('Cannot capture this tab');
  }
  
  // Request capture with user gesture context
  return chrome.tabCapture.capture({
    audio: true,
    video: true
  });
}

Conclusion

The Chrome Extension Tab Capture API is an incredibly powerful tool that enables a wide range of creative use cases. From building screen recording tools to creating collaborative applications, this API provides the foundation for rich media experiences within Chrome extensions.

Key takeaways from this guide:

With these patterns and best practices, you’re well-equipped to build robust tab capture extensions that provide excellent user experiences while respecting browser security and performance considerations.

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