diff --git a/apps/extension/entrypoints/background.ts b/apps/extension/entrypoints/background.ts index 6f0ca733..90d64b78 100644 --- a/apps/extension/entrypoints/background.ts +++ b/apps/extension/entrypoints/background.ts @@ -1,4 +1,5 @@ import { + COUNTDOWN_SECONDS, onMessage, sendMessage, type CaptureContext, @@ -113,7 +114,6 @@ async function releaseCameraBubble(): Promise { // Tab hosting the invisible camera+mic grant frame (one combined prompt). let grantTabId: number | undefined; -const COUNTDOWN_SECONDS = 3; // Pending pre-roll resolver; the overlay's skip/cancel raw message settles it // early, the worker's own clock settles it otherwise. let countdownSettle: ((result: CountdownResult) => void) | undefined; @@ -132,6 +132,7 @@ async function runPageCountdown(): Promise { } catch { return "go"; } + void sendMessage("countdownStarted", undefined).catch(() => {}); // The worker owns the clock; the overlay only displays it. First settle // wins — a late page message after the timeout resolves into a no-op. const result = await new Promise((resolve) => { diff --git a/apps/extension/entrypoints/offscreen/main.ts b/apps/extension/entrypoints/offscreen/main.ts index 4336b749..99c42b74 100644 --- a/apps/extension/entrypoints/offscreen/main.ts +++ b/apps/extension/entrypoints/offscreen/main.ts @@ -1,4 +1,4 @@ -import { onMessage, sendMessage } from "@/lib/messaging"; +import { COUNTDOWN_SECONDS, onMessage, sendMessage } from "@/lib/messaging"; import { deleteActiveRecording, pauseActiveRecording, @@ -13,9 +13,40 @@ import { * reason — the spec's user-activation requirement isn't enforced for offscreen * docs. */ +/* + * One beep per visual tick, played here rather than in the page: no autoplay + * gate, and cancelling on resolve guarantees nothing bleeds into the first + * recorded frame. Scheduled off countdownStarted (the overlay's actual mount) + * so the beeps line up with the numerals, not with the message round-trip. + */ +const beep = new Audio(chrome.runtime.getURL("/countdown-beep.mp3")); +let beepTimers: number[] = []; + +function cancelBeeps(): void { + for (const timer of beepTimers) clearTimeout(timer); + beepTimers = []; + beep.pause(); +} + +onMessage("countdownStarted", () => { + cancelBeeps(); + beepTimers = Array.from({ length: COUNTDOWN_SECONDS }, (_, i) => + window.setTimeout(() => { + beep.currentTime = 0; + void beep.play().catch(() => {}); + }, i * 1000), + ); +}); + onMessage("beginCapture", ({ data }) => recordAndUpload(data, { - onCountdown: () => sendMessage("runCountdown", undefined), + onCountdown: async () => { + try { + return await sendMessage("runCountdown", undefined); + } finally { + cancelBeeps(); + } + }, onStatus: (status) => void sendMessage("recordingStatus", status), onResult: (result) => void sendMessage("recordingResult", result), onActiveUpload: (upload) => void sendMessage("activeUploadChanged", upload), diff --git a/apps/extension/lib/messaging.ts b/apps/extension/lib/messaging.ts index e9a88c2d..43507349 100644 --- a/apps/extension/lib/messaging.ts +++ b/apps/extension/lib/messaging.ts @@ -11,6 +11,8 @@ export type StartResult = { ok: true } | { ok: false; error: string }; export type CountdownResult = "go" | "cancel"; +export const COUNTDOWN_SECONDS = 3; + export type CaptureContext = { deviceId: string; token: string; @@ -51,8 +53,10 @@ type ProtocolMap = { deleteRecording(): void; beginCapture(ctx: CaptureContext): void; // Offscreen docs can't inject page UI, so the SW renders the pre-roll - // countdown and reports how it ended. + // countdown and reports how it ended. countdownStarted marks the overlay's + // actual mount, so the offscreen beeps line up with the visual ticks. runCountdown(): CountdownResult; + countdownStarted(): void; stopCapture(): void; pauseCapture(): void; resumeCapture(): void; diff --git a/apps/extension/lib/overlay/countdown-overlay.ts b/apps/extension/lib/overlay/countdown-overlay.ts index 5f308ceb..170290d5 100644 --- a/apps/extension/lib/overlay/countdown-overlay.ts +++ b/apps/extension/lib/overlay/countdown-overlay.ts @@ -16,9 +16,13 @@ export function mountCountdownOverlay(seconds: number, rootId: string): void { const root = document.createElement("div"); root.id = rootId; + // Same backdrop as the recorder overlay's; inlined because the serialized + // injection can't reach module scope. root.style.cssText = "position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" + "justify-content:center;gap:40px;pointer-events:none;" + + "background:rgba(10,11,14,.45);" + + "backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);" + "font-family:system-ui,-apple-system,sans-serif;"; const finish = (verdict: "cancel" | "skip" | null): void => { diff --git a/apps/extension/package.json b/apps/extension/package.json index cf1f8cca..eccf1ac3 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@captureflow/extension", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "description": "CaptureFlow browser screen recorder (WXT, MV3).", diff --git a/apps/extension/public/countdown-beep.mp3 b/apps/extension/public/countdown-beep.mp3 new file mode 100644 index 00000000..ec353f83 Binary files /dev/null and b/apps/extension/public/countdown-beep.mp3 differ diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6c32c112..4b54ab49 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -3,7 +3,6 @@ import { redirect } from "next/navigation"; import { loadSession } from "@/lib/session-guard"; import { getStarCount, formatStars } from "@/lib/github"; import { Nav } from "@/components/marketing/nav"; -import { CornerBanner } from "@/components/marketing/corner-banner"; import { HeroSection } from "@/components/marketing/hero-section"; import { ModesIntro } from "@/components/marketing/modes-intro"; import { CollaborationSection } from "@/components/marketing/collaboration-section"; @@ -62,7 +61,6 @@ export default async function RootPage() { -