Skip to content

feat(web-recorder): add @devolutions/web-recorder session capture library#1824

Merged
Benoît Cortier (CBenoit) merged 10 commits into
masterfrom
feat/web-recorder
Jun 17, 2026
Merged

feat(web-recorder): add @devolutions/web-recorder session capture library#1824
Benoît Cortier (CBenoit) merged 10 commits into
masterfrom
feat/web-recorder

Conversation

@irvingoujAtDevolution

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Solves two problems:

  1. 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.

  2. Canvas idle regression in newer Chromium — the old setInterval + globalAlpha=0 keepalive no longer marks the canvas dirty in Chromium 149+, producing black/empty recordings on static content. This fixes it with a requestAnimationFrame keepalive that alternates a real pixel value so the compositor always has a fresh frame.

Adds WebMRecorder (canvas → WebM over WS) and AsciiCastV2Recorder (terminal → asciicast v2 over WS). Promise-based API, no framework dependency. Already in the publish-libraries.yml npm matrix.

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
@github-actions

Copy link
Copy Markdown

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

…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.
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) marked this pull request as ready for review June 16, 2026 18:07
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) changed the title feat(webapp): add @devolutions/web-recorder session capture library feat(web-recorder): add @devolutions/web-recorder session capture library Jun 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 minimal IRecordableSession capture contract in a new webapp/packages/web-recorder library 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.yml npm build matrix to include web-recorder and updates the pnpm-lock.yaml importer 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.

Comment thread webapp/packages/web-recorder/vite.config.ts Outdated
Comment thread webapp/packages/web-recorder/src/webm-recorder.ts Outdated
Comment thread webapp/packages/web-recorder/src/webm-recorder.ts Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • webapp/pnpm-lock.yaml: Generated file

Comment thread webapp/packages/web-recorder/src/webm-recorder.ts
Comment thread webapp/packages/web-recorder/src/asciicast-v2-recorder.ts Outdated
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

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good initiative 💯

Promise-based API, no framework dependency. Already in the publish-libraries.yml npm matrix.

I like this approach!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fantastic! 🔥

@CBenoit Benoît Cortier (CBenoit) merged commit 6a8d7ab into master Jun 17, 2026
82 of 84 checks passed
@CBenoit Benoît Cortier (CBenoit) deleted the feat/web-recorder branch June 17, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants