Video as a recording track: recording.start gains video on every surface - #332
Merged
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements docs/specs/recording-video-spec.md:
recording.startgains avideooption and the engine-encoded WebM lands inside the recording zip next to the other tracks (video/<context>.webmplus avideo/index.jsonmanifest withstartedAt/offsetMs/dimensions). The separatepage.screencastAPI andvibium: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
videoomitted: record when the engine supports it (Firefox 154+, local); otherwise the recording proceeds and the stop result reportsvideoUnavailablewith the reason.video: true(or explicit dimensions): start fails fast, and the error namesvibium install --engine firefox.video: false: off.video: {remote: 'keep'}opts out for hosts the caller controls: the file stays remote andremotePathis reported in the stop result, the saved sentence, and the manifest. The refusal error teaches the opt-in.videoRangeinstead 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)
<name>-YYYYMMDD-HHMMSS.zip— a rerun never clobbers the previous artifact; the recording'sname, 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 asBuffer/bytesand 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 testruns 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 theremote: '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); anattach-videohelper to completeremote: '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 (
screencastMu→recordingMu) and the issue's proposed fix — closestopChanbefore 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.