/**
* Offscreen document: the only extension context where WebCodecs lives
* (VideoEncoder is not exposed to service workers). The background ships the
* screencast frames over a dedicated runtime port in base64 batches, this
* page encodes them into a silent WebM, verifies it with a local <video>
* element and ships the result back. A failed verification is reported as an
* error so the background falls back to the GIF path instead of uploading a
* broken file.
*/
import browser from "webextension-polyfill";
import { buildVideoWebm } from "../background/video";
import type { VideoFrame } from "../background/gif";
/** base64 (wire format) → bytes for the encoder */
function bytesFromBase64(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
/** can the local <video> element actually open what we just encoded? */
function probePlayback(blob: Blob): Promise<{ ok: boolean; error: string }> {
return new Promise((resolve) => {
const url = URL.createObjectURL(blob);
const video = document.createElement("video");
video.muted = true;
video.preload = "metadata";
const done = (ok: boolean, error: string) => {
URL.revokeObjectURL(url);
video.removeAttribute("src");
video.load();
resolve({ ok, error });
};
video.addEventListener("loadedmetadata", () => done(true, ""));
video.addEventListener("error", () =>
done(false, video.error ? `${video.error.code} ${video.error.message}` : "unknown")
);
video.src = url;
});
}
browser.runtime.onConnect.addListener((port) => {
if (port.name !== "video-encode") return;
let frames: VideoFrame[] = [];
port.onMessage.addListener(async (raw: unknown) => {
const msg = raw as {
type: string;
frames?: { at: number; b64: string; w: number; h: number }[];
crop?: import("../background/gif").CursorCropOptions | null;
};
if (msg.type === "encode_video_start") {
frames = [];
return;
}
if (msg.type === "encode_video_frames") {
for (const frame of msg.frames ?? []) {
frames.push({ at: frame.at, jpeg: bytesFromBase64(frame.b64), w: frame.w, h: frame.h });
}
return;
}
if (msg.type === "encode_video_run") {
try {
const blob = await buildVideoWebm(frames, msg.crop ?? undefined);
if (!blob) {
port.postMessage({ type: "encode_video_result", ok: false, error: "encoding produced nothing" });
return;
}
const probe = await probePlayback(blob);
if (!probe.ok) {
port.postMessage({ type: "encode_video_result", ok: false, error: `encoded video failed playback check: ${probe.error}` });
return;
}
const raw = new Uint8Array(await blob.arrayBuffer());
port.postMessage({ type: "encode_video_result", ok: true, b64: bytesToBase64(raw) });
} catch (error) {
port.postMessage({ type: "encode_video_result", ok: false, error: error instanceof Error ? error.message : String(error) });
}
}
});
});