feat(web-recorder): add @devolutions/web-recorder session capture library#1824
Conversation
Framework-agnostic browser session-recording capture (WebM video via canvas.captureStream + MediaRecorder, plus asciicast terminal) that pushes to the Gateway /jet/jrec/push endpoint. It lives beside the playback packages (shadow-player, multi-video-player) and ships on the same publish-libraries workflow. Intended as the single source of truth to replace the duplicated recorder copies currently in DVLS web and Hub web (consumer migration is a follow-up). Ref: DVLS-14621
Let maintainers know that an action is required on their side
|
…ureStream keepalive
Use canvas.captureStream(0) (manual-frame mode) plus a timer-driven requestFrame()
to push frames at a deterministic cadence, replacing the captureStream(8) +
transparent-pixel ("drawEmpty") keepalive. The old keepalive relied on canvas
dirty-tracking that stopped emitting frames on Edge 149 (empty/near-empty WebM ->
recording-policy session kills) and also mutated the shared 2D context
(globalAlpha=0). requestFrame() captures the current pixels regardless of change,
so static desktops still produce a continuous stream and the frame count is
controlled explicitly.
Ref: DVLS-14621
…ty keepalive captureStream(0) + requestFrame() (manual mode) does not feed MediaRecorder in Chromium -- it yields zero frames (verified empirically: 0 bytes vs 68 KB for auto mode), which surfaced as the recorder hanging on "waiting for the first frame". Revert to captureStream(8) (automatic capture) and replace the static-content keepalive's zero-alpha no-op (elided by Edge 149 dirty-tracking -> empty WebM) with a real 1px value change, wrapped in save()/restore() so it cannot leak globalAlpha onto the shared 2D context. Ref: DVLS-14621
captureStream only captures a frame when the canvas is composited, and compositing is driven by the rAF/vsync loop -- NOT by setInterval. With static content (no app animation) a setInterval keepalive marks the canvas dirty but it is never presented, so zero frames are captured (verified empirically on a static canvas: setInterval -> 0 frames, rAF -> 15 frames/2s). Run the 1px dirty-nudge inside requestAnimationFrame so the compositor ticks and frames flow even when the recorded canvas is static. Ref: DVLS-14621
…rary Framework-agnostic browser library that records WebM video (canvas via captureStream + MediaRecorder) and asciicast v2 terminal sessions over WebSocket to a Devolutions Gateway jrec endpoint. Public API uses Promise<void> (no RxJS dependency). Build emits .d.ts via vite-plugin-dts + tsconfig.declaration.json. CI publish already configured in publish-libraries.yml.
There was a problem hiding this comment.
Pull request overview
Adds a new @devolutions/web-recorder package to the webapp/ pnpm workspace to provide a reusable, framework-agnostic capture library (WebM canvas capture + asciicast v2 terminal capture) intended to become the single source of truth for DVLS/Hub recording capture logic.
Changes:
- Introduces
WebMRecorder,AsciiCastV2Recorder, and a minimalIRecordableSessioncapture contract in a newwebapp/packages/web-recorderlibrary built via Vite lib mode +vite-plugin-dts. - Adds packaging/publishing scaffolding (
package.dist.json+vite-plugin-static-copy,build.ps1) consistent with the existing published webapp libraries. - Extends the GitHub Actions
publish-libraries.ymlnpm build matrix to includeweb-recorderand updates thepnpm-lock.yamlimporter entries.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| webapp/pnpm-lock.yaml | Adds the new workspace importer entry for packages/web-recorder. |
| webapp/packages/web-recorder/vite.config.ts | Vite lib-mode build config, d.ts generation, and dist package.json copy step. |
| webapp/packages/web-recorder/tsconfig.json | Package TypeScript config (typecheck/noEmit) aligned with other webapp libs. |
| webapp/packages/web-recorder/tsconfig.declaration.json | Declaration-only tsconfig used by vite-plugin-dts to emit into dist/. |
| webapp/packages/web-recorder/src/webm-recorder.ts | Implements canvas captureStream + MediaRecorder WebM streaming to WS. |
| webapp/packages/web-recorder/src/recordable-session.ts | Adds the minimal recordable-session contract (shouldStartRecording). |
| webapp/packages/web-recorder/src/index.ts | Public exports for the new library surface area. |
| webapp/packages/web-recorder/src/asciicast-v2-recorder.ts | Implements asciicast v2 terminal capture with a queued WebSocket wrapper. |
| webapp/packages/web-recorder/package.json | Workspace package manifest and build scripts (typecheck + Vite build). |
| webapp/packages/web-recorder/package.dist.json | Published package manifest copied into dist/ for npm pack. |
| webapp/packages/web-recorder/build.ps1 | CI/build helper to install, build, and npm pack from dist/. |
| .github/workflows/publish-libraries.yml | Adds web-recorder to the npm build artifact matrix. |
Files not reviewed (1)
- webapp/pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Removes the '| void' union — callers must always return a cleanup function. Cleaner contract, no biome suppression needed, prevents callback accumulation on start()/stop() cycles.
- handleMediaRecorderError: explicitly stop MediaRecorder so onstop always fires (browsers don't guarantee it after onerror) - AsciiCastV2Recorder: store settle on class field so internalStop() can resolve the pending start Promise when stop() is called before the WebSocket opens
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Good initiative 💯
Promise-based API, no framework dependency. Already in the
publish-libraries.ymlnpm matrix.
I like this approach!
Krista House (kristahouse)
left a comment
There was a problem hiding this comment.
This is fantastic! 🔥
Solves two problems:
Unified recorder — DVLS web and Hub web each maintain their own copy of the recorder (~500 lines, ~92% identical and starting to diverge). This package is the single source of truth so fixes land once for both.
Canvas idle regression in newer Chromium — the old
setInterval+globalAlpha=0keepalive no longer marks the canvas dirty in Chromium 149+, producing black/empty recordings on static content. This fixes it with arequestAnimationFramekeepalive that alternates a real pixel value so the compositor always has a fresh frame.Adds
WebMRecorder(canvas → WebM over WS) andAsciiCastV2Recorder(terminal → asciicast v2 over WS). Promise-based API, no framework dependency. Already in thepublish-libraries.ymlnpm matrix.