Claude Skills Guide

Chrome Extension Screen Recorder for Meetings: A Developer Guide

Building a Chrome extension for screen recording during meetings opens up powerful possibilities for developers and power users who need to capture, review, and share meeting content. Whether you’re creating documentation, conducting code reviews, or archiving important discussions, a well-built screen recorder extension provides significant value.

This guide walks you through building a functional Chrome extension that captures screen, audio, and meeting content using modern web APIs.

Understanding Chrome Screen Recording Capabilities

Chrome provides the getDisplayMedia API, which enables web applications to request screen capture. This API is the foundation for any screen recording extension. Combined with the MediaStream Recording API, you can capture video and audio streams and save them as video files.

The key APIs you’ll work with include:

Chrome extensions benefit from additional capabilities compared to regular web apps. Extensions can use background service workers for continuous recording, popup interfaces for quick controls, and context menus for recording initiation.

Project Structure

Create your extension with the following structure:

screen-recorder/
├── manifest.json
├── popup.html
├── popup.js
├── background.js
├── content.js
├── recorder.js
└── icons/
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

Setting Up the Manifest

Your manifest.json declares the necessary permissions for screen capture and file storage:

{
  "manifest_version": 3,
  "name": "Meeting Screen Recorder",
  "version": "1.0",
  "description": "Record screen and audio during meetings",
  "permissions": [
    "storage",
    "downloads",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "*://*.zoom.us/*",
    "*://*.meet.google.com/*",
    "*://*.teams.microsoft.com/*",
    "*://*.webex.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

The host_permissions section includes common meeting platforms. Adjust these based on your target use cases.

Implementing the Core Recorder

Create a recorder.js module that handles the recording logic:

class MeetingRecorder {
  constructor() {
    this.mediaRecorder = null;
    this.recordedChunks = [];
    this.stream = null;
    this.startTime = null;
  }

  async startRecording(options = {}) {
    const defaultOptions = {
      video: true,
      audio: true,
      systemAudio: false
    };

    const finalOptions = { ...defaultOptions, ...options };

    try {
      // Request screen capture
      const displayStream = await navigator.mediaDevices.getDisplayMedia({
        video: {
          displaySurface: 'monitor'
        },
        audio: finalOptions.systemAudio
      });

      // Combine with microphone if requested
      if (finalOptions.audio) {
        const audioStream = await navigator.mediaDevices.getUserMedia({
          audio: {
            echoCancellation: true,
            noiseSuppression: true,
            sampleRate: 44100
          }
        });

        // Combine audio tracks
        const audioTracks = audioStream.getAudioTracks();
        displayStream.addTrack(audioTracks[0]);
      }

      this.stream = displayStream;
      this.recordedChunks = [];
      this.startTime = Date.now();

      // Set up MediaRecorder
      const mimeType = this.getSupportedMimeType();
      this.mediaRecorder = new MediaRecorder(this.stream, {
        mimeType,
        videoBitsPerSecond: 2500000 // 2.5 Mbps
      });

      this.mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          this.recordedChunks.push(event.data);
        }
      };

      // Handle stream ending (user stops sharing)
      this.stream.getVideoTracks()[0].onended = () => {
        this.stopRecording();
      };

      this.mediaRecorder.start(1000); // Collect data every second
      return true;

    } catch (error) {
      console.error('Recording failed:', error);
      throw error;
    }
  }

  getSupportedMimeType() {
    const types = [
      'video/webm;codecs=vp9,opus',
      'video/webm;codecs=vp8,opus',
      'video/webm'
    ];

    for (const type of types) {
      if (MediaRecorder.isTypeSupported(type)) {
        return type;
      }
    }
    return 'video/webm';
  }

  stopRecording() {
    return new Promise((resolve) => {
      if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') {
        resolve(null);
        return;
      }

      this.mediaRecorder.onstop = () => {
        const blob = new Blob(this.recordedChunks, {
          type: this.getSupportedMimeType()
        });

        const duration = Math.round((Date.now() - this.startTime) / 1000);
        const filename = `meeting-recording-${Date.now()}.webm`;

        resolve({
          blob,
          filename,
          duration,
          startTime: this.startTime
        });
      };

      // Stop all tracks
      this.stream.getTracks().forEach(track => track.stop());
      this.mediaRecorder.stop();
    });
  }

  pauseRecording() {
    if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
      this.mediaRecorder.pause();
    }
  }

  resumeRecording() {
    if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
      this.mediaRecorder.resume();
    }
  }
}

Building the Popup Interface

Create popup.html for user controls:

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      width: 320px;
      padding: 16px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }
    .status {
      padding: 8px 12px;
      border-radius: 6px;
      margin-bottom: 12px;
      font-size: 14px;
    }
    .status.inactive {
      background: #f3f4f6;
      color: #374151;
    }
    .status.recording {
      background: #fee2e2;
      color: #991b1b;
    }
    .btn {
      width: 100%;
      padding: 10px;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      margin-bottom: 8px;
    }
    .btn-primary {
      background: #2563eb;
      color: white;
    }
    .btn-primary:hover {
      background: #1d4ed8;
    }
    .btn-danger {
      background: #dc2626;
      color: white;
    }
    .btn-danger:hover {
      background: #b91c1c;
    }
    .options {
      margin: 12px 0;
    }
    .options label {
      display: block;
      margin: 8px 0;
      font-size: 13px;
    }
    .timer {
      text-align: center;
      font-size: 24px;
      font-weight: 600;
      margin: 12px 0;
      font-variant-numeric: tabular-nums;
    }
    input[type="checkbox"] {
      margin-right: 8px;
    }
  </style>
</head>
<body>
  <div id="status" class="status inactive">Ready to record</div>
  
  <div id="timer" class="timer" style="display: none;">00:00:00</div>
  
  <div class="options">
    <label>
      <input type="checkbox" id="includeAudio" checked>
      Include microphone audio
    </label>
    <label>
      <input type="checkbox" id="includeSystem">
      Include system audio
    </label>
  </div>
  
  <button id="startBtn" class="btn btn-primary">Start Recording</button>
  <button id="stopBtn" class="btn btn-danger" style="display: none;">Stop Recording</button>
  
  <script src="popup.js"></script>
</body>
</html>

Implementing Popup Logic

Create popup.js to connect the UI with the recorder:

let recorder = null;
let timerInterval = null;

document.addEventListener('DOMContentLoaded', () => {
  loadState();
  
  document.getElementById('startBtn').addEventListener('click', startRecording);
  document.getElementById('stopBtn').addEventListener('click', stopRecording);
});

async function startRecording() {
  const includeAudio = document.getElementById('includeAudio').checked;
  const includeSystem = document.getElementById('includeSystem').checked;

  try {
    // Send message to background to start recording
    const response = await chrome.runtime.sendMessage({
      action: 'startRecording',
      options: { audio: includeAudio, systemAudio: includeSystem }
    });

    if (response.success) {
      updateUI('recording');
      startTimer();
    }
  } catch (error) {
    alert('Failed to start recording: ' + error.message);
  }
}

async function stopRecording() {
  try {
    const response = await chrome.runtime.sendMessage({
      action: 'stopRecording'
    });

    if (response.success && response.file) {
      // Download the recording
      await chrome.downloads.download({
        url: response.file.url,
        filename: response.file.filename,
        saveAs: true
      });
      
      updateUI('inactive');
      stopTimer();
    }
  } catch (error) {
    alert('Failed to save recording: ' + error.message);
  }
}

function updateUI(state) {
  const status = document.getElementById('status');
  const startBtn = document.getElementById('startBtn');
  const stopBtn = document.getElementById('stopBtn');
  const timer = document.getElementById('timer');

  if (state === 'recording') {
    status.textContent = 'Recording in progress';
    status.className = 'status recording';
    startBtn.style.display = 'none';
    stopBtn.style.display = 'block';
    timer.style.display = 'block';
  } else {
    status.textContent = 'Ready to record';
    status.className = 'status inactive';
    startBtn.style.display = 'block';
    stopBtn.style.display = 'none';
    timer.style.display = 'none';
  }
}

function startTimer() {
  const timerEl = document.getElementById('timer');
  const startTime = Date.now();
  
  timerInterval = setInterval(() => {
    const elapsed = Math.floor((Date.now() - startTime) / 1000);
    const hours = Math.floor(elapsed / 3600);
    const minutes = Math.floor((elapsed % 3600) / 60);
    const seconds = elapsed % 60;
    
    timerEl.textContent = 
      String(hours).padStart(2, '0') + ':' +
      String(minutes).padStart(2, '0') + ':' +
      String(seconds).padStart(2, '0');
  }, 1000);
}

function stopTimer() {
  if (timerInterval) {
    clearInterval(timerInterval);
    timerInterval = null;
  }
}

async function loadState() {
  // Check if recording is already in progress
  const result = await chrome.storage.local.get(['recordingState']);
  if (result.recordingState === 'recording') {
    updateUI('recording');
    startTimer();
  }
}

Handling Background Tasks

Create background.js to manage the recorder instance:

let currentRecorder = null;

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'startRecording') {
    handleStartRecording(message.options)
      .then(result => sendResponse({ success: true, result }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true; // Keep message channel open for async response
  }

  if (message.action === 'stopRecording') {
    handleStopRecording()
      .then(result => sendResponse({ success: true, file: result }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true;
  }
});

async function handleStartRecording(options) {
  currentRecorder = new MeetingRecorder();
  await currentRecorder.startRecording(options);
  
  await chrome.storage.local.set({ recordingState: 'recording' });
  return { started: true };
}

async function handleStopRecording() {
  if (!currentRecorder) {
    throw new Error('No recording in progress');
  }

  const result = await currentRecorder.stopRecording();
  
  // Create object URL for download
  const url = URL.createObjectURL(result.blob);
  
  await chrome.storage.local.set({ recordingState: 'inactive' });
  currentRecorder = null;
  
  return {
    url,
    filename: result.filename
  };
}

Key Implementation Considerations

When building a screen recorder for meetings, several factors require attention:

Platform Compatibility: Different meeting platforms have varying levels of DOM accessibility. Some provide transcript elements you can capture, while others require more creative approaches using screen capture alone.

Storage Management: Video files grow quickly. Implement cleanup logic to remove old recordings from local storage. The chrome.storage.local has a 10MB limit, so you’ll need to use chrome.downloads or external storage for larger files.

Permissions Flow: Users must explicitly grant screen capture permission. The browser’s picker dialog cannot be customized, so provide clear instructions about what to expect.

Audio Handling: System audio capture requires additional permissions and behaves differently across operating systems. macOS requires specific screen recording permissions in System Preferences.

Privacy and Ethics

When building recording tools, always consider privacy implications. Implement clear indicators when recording is active, respect platform terms of service, and consider adding features like automatic pause when sensitive content appears.

Summary

Building a Chrome extension for meeting screen recording combines several powerful APIs into a useful productivity tool. The core implementation uses getDisplayMedia for capture, MediaRecorder for encoding, and Chrome’s download API for saving files.

This guide provides a foundation you can extend with features like automatic transcription integration, cloud storage sync, or meeting timestamp markers. The modular architecture allows you to customize functionality based on your specific meeting workflows.


Built by theluckystrike — More at zovo.one