Skip to content

Video as a recording track: recording.start gains video on every surface - #332

Merged
hugs merged 11 commits into
mainfrom
recording-video
Aug 9, 2026
Merged

Video as a recording track: recording.start gains video on every surface#332
hugs merged 11 commits into
mainfrom
recording-video

Conversation

@hugs

@hugs hugs commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Implements docs/specs/recording-video-spec.md: recording.start gains a video option and the engine-encoded WebM lands inside the recording zip next to the other tracks (video/<context>.webm plus a video/index.json manifest with startedAt/offsetMs/dimensions). The separate page.screencast API and vibium:screencast.* wire commands are removed — they never shipped in a tagged release. All seven surfaces land together: wire/router, JS (async+sync), Python (async+sync), Java, CLI, MCP, and the vibe-check skill.

Semantics

  • video omitted: record when the engine supports it (Firefox 154+, local); otherwise the recording proceeds and the stop result reports videoUnavailable with the reason. video: true (or explicit dimensions): start fails fast, and the error names vibium install --engine firefox. video: false: off.
  • Remote connections record every track except video by default — the screencast is never started, nothing is left on the remote host. video: {remote: 'keep'} opts out for hosts the caller controls: the file stays remote and remotePath is reported in the stop result, the saved sentence, and the manifest. The refusal error teaches the opt-in.
  • Session close with an active recording auto-finalizes to the declared path. Chunk artifacts carry videoRange instead of a video file. A dead screencast still delivers the zip, with the error recorded in the manifest.

Defaults and result shape (each decided in review, spec amended to match)

  • The default destination is a timestamped <name>-YYYYMMDD-HHMMSS.zip — a rerun never clobbers the previous artifact; the recording's name, sanitized, seeds the stem. Explicit paths are honored exactly. The CLI resolves defaults against the user's shell, not the daemon's cwd; pathless MCP recordings land in the server's working directory when it is real and writable, else ~/Documents/Vibium.
  • stop()/stopChunk() return a result object — {path, steps, durationMs, videos | videoUnavailable} — on every client; the zip bytes ride along only for bytes-only recordings (path: null). No more buffering the archive to hand back bytes nobody asked for.

Breaking client changes (worth a release-notes call-out): stop() previously returned the zip as Buffer/bytes and wrote no file by default; it now returns the result object and the default writes a timestamped zip.

Docs: the recording tutorial now leads with where the file goes, runs every example against var.parts with semantic locators, and embeds a real recorded checkout journey (docs/images/recording-sample.webm, exactly as Firefox encoded it into the zip). The record-video how-to owns engine/remote specifics; the media-channel spec gains the empirically verified BiDi connection model (one session per browser; chromedriver multiplexes connections, Firefox refuses).

Verification: five full make test runs green at the decision gates (~5m48s each), including real Firefox 154 beta video e2e (WebM lands in the zip, manifest correct, trace tooling unaffected); the remote/serve topology exercised live for both the refusal and the remote: 'keep' path; unit coverage for option parsing, zip/manifest shapes, stem sanitization, summary/sentence, and remote-keep via a fake session.

Deferred, with rationale recorded in the spec notes: the crash-durability spool (<path>.parts/ + vibium record recover); vibium play (specced separately — the stop sentence gains its — view: suffix when it lands); an attach-video helper to complete remote: 'keep' artifacts after retrieval; media-channel Phase 3 reconciliation with this video shape.

Closes #310. Closes #311. Closes #317. Closes #319.

Touches #316 without fixing it: the stall pattern moved (screencastMurecordingMu) and the issue's proposed fix — close stopChan before draining the mutex — now conflicts with auto-finalize-on-close, which needs the connection alive to deliver the recording. #316 needs a design that aborts fast on dead browsers but still finalizes on live ones.

hugs added 11 commits August 8, 2026 00:47
… surfaces

recording.start gains a video option (bool | {width, height, frameRate});
the engine-encoded WebM lands inside record.zip next to the other tracks
(video/<context>.webm plus a video/index.json manifest carrying startedAt,
offsetMs, and dimensions). The separate page.screencast API and the
vibium:screencast.* wire commands are removed — they never shipped in a
tagged release.

Wire/router: vibium:recording.start takes video and path. Required video
fails fast when the engine can't deliver (Chrome, Firefox <154, --connect);
omitted video degrades and the stop result reports videoUnavailable with
the engine's reason. The stop result carries {path, steps, durationMs,
videos | videoUnavailable}. The path is declared at start (default
record.zip, resolved by clients; null = bytes-only); stop.path overrides.
Closing the session with an active recording auto-finalizes to the declared
path; engine temp files are deleted on close and when an abandoned
recording is superseded. If the screencast dies mid-recording the zip still
delivers, with the video absent or partial and the error recorded in the
manifest. Chunk artifacts carry no video file; their manifest records
videoRange into the session video.

Surfaces (one logic in the binary, seven mappings):
- JS async+sync: video and path options; Screencast classes removed
- Python async+sync: video= / path= kwargs (snake_case keys map to the
  wire's camelCase); screencast modules removed
- Java: RecordingOptions.video()/videoSize()/videoFrameRate()/path()
  flat fluent setters mapped onto the nested wire param
- CLI: record start --video / --video-size / --video-fps / -o; stop
  prints "Saved record.zip (23 steps, 14s video)"
- MCP: browser_record_start gains video, video_width, video_height,
  video_frame_rate, path; stop returns the same saved sentence
- vibe-check skill documents recording-with-video

Closes #310.
The tutorial still taught bytes-by-default stop() with a manual file
write, listed screenshots as default-off, claimed a ~100ms screenshot
loop and snapshot-on-stop, and had no Java, MCP, video, or start-path
coverage. Examples now lean on the real defaults (screenshots on,
record.zip delivery), a Video section points at the how-to guide, Java
and MCP join the Quick Start and reference, and the two recording docs
cross-link with an explicit scope split: tutorial = recording in
general, how-to = the video track's engine specifics.
vibium record start without -o declared record.zip relative to the
daemon's working directory, which is not the user's shell (#119 fixed
the same surprise for stop -o). The CLI now resolves the default before
it goes over the socket, so the zip lands where the user ran the
command.

The MCP/CLI start line also no longer inlines the full engine reason
when video is unavailable — the sentence stays compact and the full
actionable reason still arrives in the stop result.
…d name-seeded

stop() no longer buffers the zip to hand back bytes nobody asked for.
Every client (JS async+sync, Python async+sync, Java) now returns
{path, steps, durationMs, videos | videoUnavailable}; the zip bytes
ride along only for bytes-only recordings (path null at start), where
they are the point. The Java file read-back is gone with it. stopChunk
returns the same shape.

The default destination is record-YYYYMMDD-HHMMSS.zip — a rerun never
clobbers the previous artifact; same-second collisions get a -2
suffix. The recording's name, sanitized, seeds the stem:
start({name: 'login'}) lands login-20260808-094123.zip. Explicit
paths are honored exactly and overwrite. The CLI resolves the default
against the user's shell, not the daemon's working directory (#119).

The recording-video spec records both decisions; the vibium play spec
gains the no-arg form: `vibium play` opens the most recently modified
recording zip in the working directory — the counterpart to
timestamped names.
The tutorial now leads with where the recording file goes (timestamped
names, the three ways to control them, Record Player) instead of
burying it at the end, and every example runs against var.parts with
semantic locators — find by text, role, and label instead of the
attribute selectors that make tests brittle. The one CSS fallback (the
cart icon has no accessible name) is labeled as exactly that.

The Action Groups example is now a full checkout journey — browse, add
to cart, fill the checkout form, proceed to payment — verified against
the live shop, and the embedded sample video is that same journey as
Firefox encoded it into the recording zip (23 steps, 8 seconds).
Java and MCP join the Quick Start and reference.
A pathless browser_record_start landed wherever the MCP server process
happened to start. Now it lands in the server's working directory when
that is a real, writable place — hosts like Claude Code launch the
server in the project, so the recording sits next to the code — and in
~/Documents/Vibium otherwise. Same reasoning as the screenshots'
~/Pictures/Vibium default (#119): an MCP caller may have no working
directory to reason about. Recordings are work artifacts, so Documents,
not Pictures or Movies.

Explicit paths and the CLI (which resolves in the user's shell) are
unchanged.
The design premise said a second BiDi WebSocket to the same browser
"handles concurrent connections fine". Measured 2026-08-08: the spec
allows at most one session per browser and leaves multi-connection
sessions an open issue; chromedriver multiplexes a second WebSocket
onto an existing session's webSocketUrl and serves both concurrently,
while Firefox 154 accepts the connection but offers no way to attach
it ("Maximum number of active sessions" / "invalid session id"). The
media channel is Chrome-only until Firefox grows multi-connection
sessions; Firefox always takes the synchronous fallback, and its
native screencast covers video there.
…t behind

"Vibium cannot retrieve that file" read as if a video lands on the
remote host for the user to fetch by hand. What happens: the screencast
is never started over --connect, no file is written anywhere, and the
stop result reports videoUnavailable. Say that, and say the part that
matters — every other track records fully over remote connections. The
tutorial's video section gains the one-line version.
…the file there

The remote refusal was vibium's own protection, not an engine limit: the
engine records happily, vibium just can't move or clean the file. For
hosts the caller controls, video: {remote: 'keep'} opts out — the
screencast records normally and the file stays on the remote host, with
remotePath reported in the stop result, the saved sentence, and the
zip's video/index.json (which keeps offsetMs/startedAt/dimensions so
the artifact can be completed after retrieval). Vibium never reads,
moves, or deletes a remote-keep path — including the hazard where it
happens to name an unrelated local file. Retrieval and cleanup are the
caller's.

The refusal error now teaches the opt-in at the moment it's needed.
Surfaces: JS/Python pass the wire shape through with types and docs;
Java gains .videoRemote("keep"); CLI --video-remote keep; MCP
video_remote. Verified end-to-end through the serve topology: the stop
result named the remote path and a real WebM sat there.
The old handleScreencastStop silently ignored the os.Remove result
after inline delivery (#317). That handler is gone, but its successor
in Recorder.Stop/RemoveEngineFile carried the same silent ignore; a
failed delete now leaves a debug trace naming the leaked path.
…ideo

GitHub strips <video> tags for repo-committed files, so the tutorial's
sample rendered as nothing but its caption link. A 640px GIF preview
(1.0 MB) renders inline everywhere; the caption keeps the link to the
real WebM, which is the honest artifact.
@hugs
hugs merged commit d8409e3 into main Aug 9, 2026
1 check passed
@hugs
hugs deleted the recording-video branch August 9, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant