feat(core): capture a whole deck as video from the CLI - #2
Open
desek wants to merge 3 commits into
Open
Conversation
Adds `open-slide video`, which drives headless Chromium through the viewer and writes a single MP4 per deck. Animations are paused from document start and seeked by `currentTime` per frame, so existing CSS-keyframe decks are capturable with no authoring change. Inter-page transitions are captured from the viewer rather than composed at encode time, which keeps one definition of a transition and makes the output match what an audience sees. Step-gated decks fall back to one MP4 per page, reported with guidance on authoring a loop variant. Also fixes malformed CLI flags exiting 1 instead of 2. Commander only converts InvalidArgumentError into a code-2 exit, and its own default usage exit is 1, so this needs both the parser change and a program-level exitOverride. Applies to `export` as well, hence the separate patch changeset.
Adds the CLI reference page alongside the other subcommands, and corrects the loop-variant authoring reference, which was written before a loop variant existed and carried three claims that did not survive building one.
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.
Adds
open-slide video, a headless CLI subcommand that captures a whole deck as a single continuous MP4.Stacked on 1weiho#379 — this targets
feat/export-slides-as-pngand reuses the capture pipeline that PR establishes. Review after it lands, or read the three commits here in isolation.Motivation
A deck is currently shareable three ways: presented live, exported as static HTML, or exported as PDF. All three require a viewer to drive the deck themselves, and none of them carry the animation an author spent effort on.
A recording is the missing distribution format. Release notes, README headers, docs pages, and social posts all take video and none of them take a deck. The workaround today is to screen-record present mode, which is the wrong tool: a screen capture samples whenever the compositor delivered a frame, so it drops and duplicates frames under load, cannot be reproduced, and bakes in whatever the recording machine was doing at the time. A re-record is never the same file, so it can be neither diffed nor regenerated in CI.
The pipeline already exists. 1weiho#379's
export.tsboots Vite in process, launches Chromium, enumerates decks, resolves targets, and writes atomically. None of that needed rebuilding. What it lacked is time:renderOnenavigates, waits for the page to settle, and screenshots once.What's included
The subcommand.
open-slide video --slide <id>produces one MP4 covering every page in order;--allproduces one per deck.Flags:
--slide/--all(mutually exclusive),--page(requires--slide),--out,--fps(1–120, default 30),--duration,--dwell(default 1500 ms),--per-page,--keep-frames,--port,--timeout.How time works. Decks animate with wall-clock CSS keyframes and expose no playhead, so frames cannot be sampled on a timer. Every animation is paused as it appears via an init script, and
currentTimeis driven explicitly per frame. That makes existing CSS-keyframe decks seekable with no authoring change. Each page contributes its measured animation duration followed by the run-wide--dwellhold; a page with no finite animation contributes a documented fallback rather than being skipped, and a measured duration is clamped so one runaway delay cannot produce an unbounded render.Transitions are the viewer's own. Rather than compositing a crossfade in ffmpeg, the capture advances the deck in place and seeks the transition animations
SlideTransitionLayercreates. The deck's declared transitions — includingmorph— appear in the file exactly as an audience sees them, and there is no second definition of "transition" to drift from the viewer's. The cost is that capture is one continuous browsing session rather than independent page loads; ffmpeg only concatenates.Step-gated decks fall back to per-page output. A continuous session cannot cross a page with pending steps without walking its reveals. Such decks produce one MP4 per page, and the command says so rather than silently changing shape. Worth stating precisely, because an earlier draft of this CR got it wrong: a directly-loaded step page arrives with every step already revealed, so a fallback capture is missing no content — what it loses is the reveal motion and the inter-page transitions. The warning recommends authoring a loop variant and names what one entails.
Prerequisites are preflighted.
playwright-chromiumstays a devDependency reachable only through a dynamic import;ffmpegis invoked as a subprocess and never vendored. Both are checked before any work, exiting 2 with a copy-pasteable install hint. Output is H.264 /yuv420p/ 1920×1080, so it plays in browsers and system players rather than only in developer tooling.Implementation notes
The readiness signal from 1weiho#379 must not be used here, and that is counter-intuitive.
__OPEN_SLIDE_EXPORT_READYmeans every finite animation has finished — and a finished animation leavesdocument.getAnimations(), so seeking after readiness finds nothing to seek and produces the settled frame repeated N times. A valid MP4 of a still image, with no error. The PNG exporter is built to wait for motion to stop; video requires that it never start. Fonts are awaited directly instead.Three related traps, each of which produced a plausible-looking wrong file before it was found:
getAnimations(). A settled enter transition was being measured as the next page's content duration, and the per-frame seek then reset it to zero, replaying the transition underneath the page it had just joined. Settled and boundary animations are tagged, and every path that enumerates animations skips tagged ones.SlideTransition.durationis not the boundary's length.runPhaseresolves each phase asphase.duration ?? durationwith its own delay, andmorphcarries its own timing, so a boundary sized from the declared value truncates any deck that overrides either. Boundaries are measured from the animations that actually appeared.prefers-reduced-motiondeletes every transition.player.tsxpassesdisabled={prefersReducedMotion}to the transition layer, whose disabled branch swaps pages with zero animations — a valid file with no boundaries and no error. The browser context pinsreducedMotion: 'no-preference'.Determinism is qualified, deliberately. Page and dwell frames are byte-identical across runs. Transition boundary frames are not: edge antialiasing on scale-animated compositor layers rasterises to one of two stable variants per run, measured at YMAX 93 / YAVG 0.16 on
slide-transitionsacross 10 frames of 860. The acceptance criteria were split rather than weakened — byte-identity for page and dwell segments, aYMAX ≤ 160 / YAVG ≤ 1.0perceptual bound for boundaries — andRisk 12in the CR records that capturing the viewer's transitions is what introduced this. An encode-time crossfade would have been deterministic by construction; the trade was made knowingly.Pure helpers are separated from orchestration. Duration resolution, frame counting, filename derivation, output-shape selection, and deck timeline planning are exported and unit-tested without Playwright or ffmpeg.
Incidental fixes
InvalidArgumentErrorinto a code-2 exit — and its own default exit for a usage error is also 1, so the parser change alone was insufficient and a program-levelexitOverridewas needed too. This also correctsexport, hence a separatepatchchangeset. The spec that should have caught it calledvalidateFlagsdirectly and never traversed Commander; the replacement drivesrun(argv)and fails 8 of 14 against the old code.playwright-chromiumwas undeclared. feat(core): export slides as PNG from the viewer and the CLI 1weiho/open-slide#379'sexportdynamically imports it but does not list it, so a fresh clone cannot exercise that path. Added as a root devDependency here; it arguably belongs in feat(core): export slides as PNG from the viewer and the CLI 1weiho/open-slide#379.Documentation
A CLI reference page alongside the other subcommands, a README entry, and a
loop-variant.mdreference in theslide-authoringskill for authoring decks meant to be recorded. That reference already existed, written before anyone had built a loop variant, and three of its claims did not survive doing so: that the closing transition belongs on the last page (transitions attach to the incoming page, so the last page never gets an outgoing one), that infinite animations suit ambient motion (the runtime remounts the outgoing page, restarting them at zero at every cut), and that a page with no finite animation costs only the dwell. It gains a "snapshot contract" section covering the failure modes that only appear in motion.CR-0003and its validation report follow thedocs/cr/convention from 1weiho#379. The iterate ledger is a new artifact type recording the last mile across 18 attempts, including the ones that failed. It references a demo deck used to exercise the capture which is deliberately not included in this branch, so several of its later entries describe code that is not in the diff.Testing
pnpm testpnpm typecheckpnpm checkpnpm buildManual verification:
ffprobeconfirms H.264 /yuv420p/ 1920×1080 / 30fps on both output shapes; the built runtime bundle contains no staticplaywright-chromiumreference; a step-gated deck warns, writes per-page files, and exits 0; determinism measured across three captures per deck as described above.