createScreenRecorder function exported

Last updated: 2026-03-05T10:53:28.864Z

Metrics

LOC: 183 Complexity: 17 Params: 0

Signature

createScreenRecorder(): : ScreenRecorder

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'createScreenRecorder' has cyclomatic complexity 17 (max 10)
  • [warning] max-lines: 'createScreenRecorder' has 183 lines (max 80)

Source Code

export function createScreenRecorder(): ScreenRecorder {
  let state: RecordingState = "inactive";
  let recorder: MediaRecorder | null = null;
  let stream: MediaStream | null = null;
  let chunks: Blob[] = [];

  function mimeForCodec(codec: "vp8" | "vp9"): string {
    const candidate = `video/webm;codecs=${codec}`;
    if (
      typeof MediaRecorder !== "undefined" &&
      MediaRecorder.isTypeSupported(candidate)
    ) {
      return candidate;
    }
    return "video/webm";
  }

  async function acquireStream(
    tabId: number,
    opts: ScreenRecordOptions,
  ): Promise<MediaStream> {
    // chrome.tabCapture.getMediaStreamId requires an active tab
    const streamId = await new Promise<string>((resolve, reject) => {
      chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, (id) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve(id);
        }
      });
    });

    const constraints: MediaStreamConstraints = {
      audio: opts.includeAudio
        ? ({
            mandatory: {
              chromeMediaSource: "tab",
              chromeMediaSourceId: streamId,
            },
          } as MediaTrackConstraints)
        : false,
      video: {
        mandatory: {
          chromeMediaSource: "tab",
          chromeMediaSourceId: streamId,
        },
      } as MediaTrackConstraints,
    };

    return navigator.mediaDevices.getUserMedia(constraints);
  }

  return {
    get state() {
      return state;
    },

    async start(tabId, optionsOverride) {
      if (state !== "inactive") {
        log.warn("Recording already active");
        return;
      }

      const opts: ScreenRecordOptions = {
        ...DEFAULT_SCREEN_RECORD_OPTIONS,
        ...optionsOverride,
      };

      try {
        stream = await acquireStream(tabId, opts);
        chunks = [];

        const mimeType = mimeForCodec(opts.codec);
        recorder = new MediaRecorder(stream, {
          mimeType,
          videoBitsPerSecond: opts.videoBitrate,
        });

        recorder.ondataavailable = (e: BlobEvent) => {
          if (e.data.size > 0) chunks.push(e.data);
        };

        recorder.onerror = (e) => {
          log.error("MediaRecorder error:", e);
          state = "inactive";
        };

        recorder.start(1000); // 1 s timeslice for progressive encoding
        state = "recording";
        log.info(`Recording started (tab ${tabId}, codec=${opts.codec})`);
      } catch (err) {
        log.error("Failed to start recording:", err);
        state = "inactive";
        throw err;
      }
    },

    async startWithStreamId(streamId, optionsOverride) {
      if (state !== "inactive") {
        log.warn("Recording already active");
        return;
      }

      const opts: ScreenRecordOptions = {
        ...DEFAULT_SCREEN_RECORD_OPTIONS,
        ...optionsOverride,
      };

      const constraints: MediaStreamConstraints = {
        audio: opts.includeAudio
          ? ({
              mandatory: {
                chromeMediaSource: "tab",
                chromeMediaSourceId: streamId,
              },
            } as MediaTrackConstraints)
          : false,
        video: {
          mandatory: {
            chromeMediaSource: "tab",
            chromeMediaSourceId: streamId,
          },
        } as MediaTrackConstraints,
      };

      try {
        stream = await navigator.mediaDevices.getUserMedia(constraints);
        chunks = [];

        const mimeType = mimeForCodec(opts.codec);
        recorder = new MediaRecorder(stream, {
          mimeType,
          videoBitsPerSecond: opts.videoBitrate,
        });

        recorder.ondataavailable = (e: BlobEvent) => {
          if (e.data.size > 0) chunks.push(e.data);
        };

        recorder.onerror = (e) => {
          log.error("MediaRecorder error:", e);
          state = "inactive";
        };

        recorder.start(1000);
        state = "recording";
        log.info(`Recording started via streamId, codec=${opts.codec}`);
      } catch (err) {
        log.error("Failed to start recording from streamId:", err);
        state = "inactive";
        throw err;
      }
    },

    async stop() {
      if (state !== "recording" || !recorder || !stream) {
        return new Blob([], { type: "video/webm" });
      }

      state = "stopping";

      return new Promise<Blob>((resolve) => {
        recorder!.onstop = () => {
          const blob = new Blob(chunks, {
            type: recorder!.mimeType || "video/webm",
          });
          log.info(`Recording stopped — ${(blob.size / 1024).toFixed(1)} KB`);

          // Release tracks
          stream!.getTracks().forEach((t) => t.stop());
          stream = null;
          recorder = null;
          chunks = [];
          state = "inactive";

          resolve(blob);
        };

        recorder!.stop();
      });
    },
  };
}

Members

Name Kind Visibility Status Signature
mimeForCodec function - mimeForCodec(codec: "vp8" | "vp9"): : string
acquireStream function - acquireStream( tabId: number, opts: ScreenRecordOptions, ): : Promise<MediaStream>
state method - state()
start method - start(tabId, optionsOverride)
startWithStreamId method - startWithStreamId(streamId, optionsOverride)
stop method - stop()

No outgoing dependencies.

Impact (Incoming)

graph LR createScreenRecorder["createScreenRecorder"] handle["handle"] handle -->|uses| createScreenRecorder style createScreenRecorder fill:#dbeafe,stroke:#2563eb,stroke-width:2px click createScreenRecorder "44f19d27f7c2aaba.html" click handle "3b3925f07e1ac5c3.html"
SourceType
handle uses