Export video on the web beta, and of cloud strategies anywhere - #230
Merged
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SunkenInTime
force-pushed
the
web-video-export
branch
6 times, most recently
from
September 29, 2026 06:46
fa30ce5 to
3b93ba2
Compare
This comment has been minimized.
This comment has been minimized.
SunkenInTime
force-pushed
the
web-video-export
branch
from
September 29, 2026 06:58
3b93ba2 to
91c254e
Compare
Browsers cannot spawn the bundled ffmpeg, so the web beta needs its own encoder. Two building blocks, not wired into the exporter yet: - Mp4H264Muxer: pure-Dart faststart MP4 writer for one H.264 track (ftyp, moov, mdat; run-length stts, stss, one chunk). Verified against ffprobe and an ffmpeg decode of a real libx264 stream. - WebCodecsMp4Encoder: VideoEncoder bindings that turn RGBA frames into a constant-frame-rate MP4, repeating each frame for its duration, scaling through an OffscreenCanvas, and waiting on the encode queue. The export exceptions move to video_export_errors.dart so the web file avoids dart:io; ffmpeg_video_encoder.dart re-exports them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Video export was blocked on the web because it encodes with a bundled ffmpeg process, which a browser cannot start. It was also broken for cloud strategies everywhere: the dialog listed pages from, and rendered, the local Hive box, where cloud strategies never are, so it showed no pages and exported nothing. Encoding moves behind VideoFrameSink. VideoExporter renders frames and hands them to a sink: - FfmpegVideoSink (desktop) is the existing PNG-sequence + ffconcat + ffmpeg pipeline, moved out of the exporter unchanged. - The browser sink encodes with WebCodecs into an MP4 as frames arrive (previous commit) and hands the bytes back, which the dialog downloads through FilePicker.saveFile(bytes:). Presets keep their sizes and bitrates; the browser has no second pass, so there is no size retry. A browser without H.264 WebCodecs is told so before anything renders. What gets rendered (loadVideoExportSource): a local strategy is saved and read back from the library as before. A cloud strategy's other pages live only on the server, so the export saves, waits (up to 20 s) for this device's changes to land, i.e. for the strategy to show as synced, and then reads the whole strategy from the server. If the work does not land it stops and says so rather than export a video missing edits the user can see. A signed-out reader who opened a share link reads through that link (fetchFullSnapshot takes the shareToken the server already accepts). Images go through the same resolution as screenshots: files as they are, cloud URLs fetched and decoded before rendering, held for the whole export and handed to the capture container through captureImageSourcesProvider. The dialog's page list comes from wherever the strategy lives, and it shows its progress dialog from the start, so saving and syncing are visible and cancellable. Video export leaves PlatformPolicy.webBeta.desktopOnly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review and browser-run follow-ups to web video export. Colour: Edge's hardware H.264 encoder writes full-range BT.709 samples with no colour description in the bitstream. Players then assume limited range and crush Icarus's darks to black (the map background went from 15 to 0). The encoder reports the colour it wrote in its decoder config, and the muxer now carries it in the sample entry's colr/nclx box, which ffmpeg, Chromium and QuickTime read. Measured in Edge: frames decode within 2 levels of the screenshot's pixels, in ffmpeg and in browser playback. Cloud source: forceSaveNow on a cloud strategy with nothing to send marks the save state pending, and since no queue state changes, nothing clears it, so waiting on canLeaveSafely could time out on a synced strategy and leave the chip saying syncing. The export now flushes the open page through the page session (which sets no chip state) and reads the op and media queues directly: nothing queued, in flight or uploading means sent. Work held for review fails at once rather than after 20 s. Cancelling now reaches the sync wait and image loading, and a cancel that arrives while the export wraps up no longer downloads the video. A selected page a teammate deleted fails the export with a message and refreshes the page list instead of being left out silently. The ffmpeg integration tests skip when ffmpeg lacks the H.264 encoder they need. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…utbox Second review follow-up. Cancelling now also stops image loading between images (and after the page flush), releasing what was already decoded, instead of downloading and decoding the rest first. And the export only counts this device's work as sent when the media outbox is readable too: an upload record it could not read is not in its job list, so an empty list alone cannot say the upload landed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alone The exporter built its offscreen container by hand, so its strategy provider built a real auth provider, which set or cleared auth on the app's one Convex client and tore it down on dispose: every video export signed the editor out of cloud sync. That was true on desktop before this branch too, for anyone signed in. It now uses the screenshot's createCaptureContainer, with its inert auth, and a test requires an export to make no call to the Convex auth API. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Greptile follow-up: the preflight asked whether the browser could encode 1080p at 8 Mbps whatever the preset and length, so a browser that could encode what the export actually needs (a long Potato video drops to 720p at 250 kbps) was refused. The check and the encoder now share one function for the output size and bitrate, and the dialog passes the planned length. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fetcher now receives the capture's client (#232); passing it to downloadCloudImageBytes keeps one finished download from closing it for the rest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SunkenInTime
force-pushed
the
web-video-export
branch
from
September 29, 2026 07:35
41879bc to
21203fc
Compare
Owner
Author
|
@greptileai review |
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.
Stacked on #229. This is the second half of "make record and screenshot work on the web".
Video export was blocked on the web for a real reason: it encodes by starting a bundled
ffmpegprocess, and a browser can't start processes. It was also broken for cloud strategies everywhere. The dialog listed pages from the local Hive box and rendered from it, where cloud strategies never are, so a cloud strategy showed no pages and exported nothing.The web now encodes H.264 with WebCodecs and writes the MP4 itself. Desktop keeps ffmpeg.
How
VideoFrameSinkseparates rendering from encoding.VideoExporterrenders frames and hands each one, with its duration, to a sink:FfmpegVideoSink(desktop) is the existing PNG-sequence, ffconcat and ffmpeg pipeline, moved out of the exporter unchanged.browser_video_sink*.dart, a conditional export ondart.library.js_interop) usesWebCodecsMp4Encoder, which encodes as frames arrive, andMp4H264Muxer, a pure-Dart faststart MP4 writer. It returns the bytes, and the dialog downloads them withFilePicker.saveFile(bytes:).dequeue), and 720p scaling goes through an OffscreenCanvas.colr/nclxbox.loadVideoExportSource):fetchFullSnapshottakes theshareTokenthe server already accepts (Let anyone with a strategy link view it without an account #225), so there's noconvex/change.createCaptureContainer. Its hand-built container used to build a real auth provider, which tore down the app's Convex auth when disposed, so every video export signed the editor out of sync. That was already true on desktop for anyone signed in. A test requires an export to make no call to the Convex auth API.PlatformPolicy.webBeta.desktopOnly.Reproduction
Verification
test/video_exporter_sink_test.dart: the exporter renders every page and transition frame, with its image, into a sink. Desktop encodes a real 1920x1080 H.264 MP4 of the planned length through the moved ffmpeg pipeline (skipped where ffmpeg lacks an H.264 encoder).test/video_export_source_test.dart: waits for queued work; a strategy with nothing to send reads at once and leaves the save chip alone; timeout; held-for-review; cancel; a deleted page; the share token; local read-back with image files.test/mp4_muxer_test.dart: box layout, thecolrbox, and a real libx264 stream muxed with variable durations that ffprobe and ffmpeg read back exactly (4.9 s, 20 frames,pc/bt709).h264 High, 1920x1080, 30/60 fps, 133/265 frames, and ffmpeg decodes both cleanly.flutter build webwith CI's flags compiles, and the bundle contains the WebCodecs path.Found along the way (not changed here)
forceSaveNowwith nothing to send marks the save state pending, and nothing clears it, so the chip can say "Syncing…" indefinitely. Export no longer goes through it; the root cause belongs in its own PR..icacloud export also callsfetchFullSnapshotwithout the share token, so it fails for signed-out link readers. It's a one-line follow-up, now that the parameter exists.Not covered
🤖 Generated with Claude Code
No outstanding findings block merging.
Summary
The PR adds browser video encoding and cloud-strategy video export. The latest changes give capture image downloads a caller-owned client, addressing the previously reported signed-URL refresh failure. No new findings were supplied, and neither previous review thread remains outstanding.
Reviews (3) · Last reviewed commit: "Download a video export's images through..."