Skip to content

feat(extension): complete the recorder — in-page UI, control bar, screenshot mode, hardening - #6

Merged
sardorml merged 10 commits into
mainfrom
feat/extension-complete
Aug 2, 2026
Merged

feat(extension): complete the recorder — in-page UI, control bar, screenshot mode, hardening#6
sardorml merged 10 commits into
mainfrom
feat/extension-complete

Conversation

@sardorml

@sardorml sardorml commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Completes the browser extension: in-page recorder UI, full recording controls, screenshot mode, and the robustness gaps from PLAN.md. Ten commits, each a self-contained unit.

What's new

Recorder surface

  • No anchored action popup: the toolbar click injects the panel as an extension iframe floating top-right over a blurred, dimmed backdrop. Backdrop click / Escape close it; restricted pages fall back to a standalone popup window.
  • Panel redesign: home / video / screenshot tabs, device rows with on-off pills and device selects, live mic level meter, 30-min limit caption, Effects/Blur stubs + More menu, real brand icon.
  • Missing camera+mic grant → one combined native prompt on open (invisible grant frame); on allow, both devices flip on and the bubble appears.

Recording controls

  • In-page control bar (content script, shadow DOM, draggable): stop, countdown timer, pause/resume, restart, delete. Survives navigation with a continuous timer.
  • Gapless pause via the engine (frame drop + timestamp shift; webcam via MediaRecorder.pause). Restart re-inits the upload without re-showing the picker. The cap counts active time only.
  • Mic without camera now records — muxed as AAC-LC into the screen fMP4 by the engine.

Screenshot mode

  • Captures the visible tab (overlay hidden for the frame) and posts the PNG to the forked /api/s/upload domain; opens the view link.

Hardening

  • POST /api/r/abort on failure/delete/restart + an SW startup sweep for crashed uploads (marker relayed to the SW — offscreen docs can't touch chrome.storage).
  • Finalize lost to the network falls back to the /api/r/state probe; part pumps wait out offline windows; 429/413/401 map to friendly copy; auth/check probe on open; wire-drift test pins the forked types against apps/web.
  • host_permissions added so extension-context fetches skip CORS/PNA (Brave blocks localhost dev fetches without them).

Web fix (dev-only symptom)

  • The screenshot viewer resolved its image URL from process.env only, so dev uploads pointed at the prod CDN and rendered broken. Now resolves through the binding env like the recording domain.

Cleanup

  • Code comments no longer cite third-party products.

Notes for review

  • The declared content script (http/https) adds the "read and change data on all websites" install warning — required for the navigation-surviving control bar.
  • The overlay iframe is deliberately opaque and sized via SW-relayed height reports: iframe transparency renders as an opaque white canvas on color-scheme-mismatched pages, and window.postMessage silently failed on some sites.
  • Deploy TODO (unchanged): set CAPTUREFLOW_EXTENSION_ID in wrangler vars before the prod auth handshake can pass.

Verification

  • pnpm typecheck (all 7 projects), extension tests 47/47, engine tests 14/14, web tests 26/26, wxt build clean, prettier clean.
  • Manually exercised in local dev: overlay open/close, combined permission prompt, screenshot capture → upload → viewer.
  • Still owed (can't automate the native picker): full recording e2e — record → pause/resume → control-bar stop → playable link with a continuous timeline; mic-only audio; restart/delete leaving no orphaned rows.

sardorml added 10 commits July 16, 2026 20:21
stream-recorder gains an optional micStream muxed as AAC-LC into the screen
fMP4 (cross-track-offset keeps A/V aligned; drops to video-only where AAC
encoding is unavailable) and pause()/resume() that stop encoding while
shifting later chunk timestamps back by the wall-clock pause, so the muxed
timeline stays gapless. Timestamps are clamped per track to stay monotonic.
The webcam recorder pauses via MediaRecorder.pause()/resume().
- abort() now releases the server-side multipart via POST /api/r/abort
  (idempotent, best-effort) instead of only clearing local buffers
- a finalize lost to the network falls back to the no-auth GET /api/r/state
  probe: state === ready means the idempotent finalize already applied
- queued parts wait out offline windows and resume on the online signal;
  a request that dies mid-flight stays fatal (desktop parity)
- 429/413/401 map to actionable copy via friendlyUploadError; checkAuth
  probes GET /api/r/auth/check so a revoked token is caught early
Textual drift guard (PLAN.md Decision 5): extracts the shared type
declarations from both files, normalises comments/whitespace, and fails on
any mismatch — without a cross-app import that would violate the boundary
rule.
The recorder runs as restartable sessions over one set of acquired streams:
restart discards the upload (server abort included) and re-inits without
re-showing the picker; delete aborts and stops the capture. Pause drives the
engine's gapless pause on both tracks and the 30-min cap only counts active
time. Without a camera the mic no longer drops — it rides the screen fMP4
as the engine's AAC track. RecordingStatus carries startedAt/paused timing
so any surface can derive the live clock, and the active-upload crash marker
is relayed to the SW (offscreen documents cannot touch chrome.storage).
Declared content script (http/https) rendering a draggable shadow-DOM bar —
stop, countdown timer, pause/resume, restart, delete — from the session-
storage recording status, so it re-mounts after navigation with a continuous
timer. Buttons disable while the tail upload saves.
… mode

The recorder UI now opens as an extension iframe floating top-right over a
blurred backdrop instead of an anchored action popup (default_popup is
stripped via a build:manifestGenerated hook; restricted pages fall back to a
standalone popup window). The iframe stays opaque and is sized to the panel
through SW-relayed height reports — iframe transparency renders as an opaque
white canvas when the host page's color-scheme differs, and window.postMessage
proved unreliable on some sites.

Panel redesign: home/video/screenshot tabs, device rows with on/off pills and
device selects, live mic level meter, recording-limit caption, Effects/Blur
stubs and a More menu, brand icon. Opening it without a camera+mic grant
injects an invisible grant frame that asks for both devices in one native
prompt; on allow both toggle on and the bubble appears (a dismissed prompt is
not treated as a block). Screenshot mode captures the visible tab (overlay
hidden for the frame) and posts the PNG to the forked /api/s/upload domain.

host_permissions now cover the API origin (localhost too in dev builds) so
extension-context fetches skip CORS/Private-Network-Access — Brave otherwise
blocks extension frames from reaching a localhost dev server. The SW also
sweeps crashed uploads on startup and opens the recording URL on success.
The extension still had placeholder icon PNGs — gen-brand-icons.py never
emitted them. Regenerated 16/32/48/128 from the canonical master and taught
the script the extension output so future regens stay in sync.
The viewer built imageUrl from lib/site's process.env-only R2_PUBLIC_BASE_URL,
which never sees the .dev.vars media-proxy override — every screenshot
uploaded against next dev rendered a broken image pointing at the prod CDN.
publicScreenshotUrlFor now resolves binding env → process.env → CDN fallback,
mirroring the recording domain's publicUrlFor. Prod output is unchanged.
Status header, superseded auth decision (externally_connectable tab sign-in),
phase 2/3 + screenshot-mode results, the in-page recorder surface, and the
hardening notes now match the implementation.
Comments now describe the behaviour on its own terms instead of citing other
products. Comment-only change; no behaviour or binary output is affected.
@sardorml
sardorml force-pushed the feat/extension-complete branch from e078b0c to f84ffc7 Compare July 16, 2026 15:23
@sardorml sardorml changed the title feat(extension): Loom-parity recorder — in-page UI, control bar, screenshot mode, hardening feat(extension): complete the recorder — in-page UI, control bar, screenshot mode, hardening Jul 16, 2026
@sardorml
sardorml marked this pull request as ready for review August 2, 2026 14:45
@sardorml
sardorml merged commit 897a08e into main Aug 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant