# Export video on the web beta, and of cloud strategies anywhere \(\#230\) · dara/icarus

[View on GitCafe](https://git.cafe/dara/icarus/commit/3cbd0f7c2b024599870ac140e29a02f962666aaa)

Repository: [dara/icarus](https://git.cafe/dara/icarus)

Visibility: public

Requested revision: 3cbd0f7c2b024599870ac140e29a02f962666aaa

Requested commit: 3cbd0f7c2b024599870ac140e29a02f962666aaa

Commit: 3cbd0f7c2b024599870ac140e29a02f962666aaa

Tree: 2ab222d5bee4c104c709b769acf6bc787f3bd060

Author: Dara Adedeji

Committer: GitHub

## Message

```
Export video on the web beta, and of cloud strategies anywhere (#230)

* Add a WebCodecs H.264 encoder and MP4 muxer for web video export

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>

* Export video on the web beta, and of cloud strategies anywhere

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>

* Say what colour web videos are, and export only what has landed

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>

* Stop a video export between images, and trust only a readable media outbox

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>

* Render video exports in the capture container that leaves cloud sync 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>

* Expect downloads to start together when a video export is cancelled

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Check browser video support at the size and bitrate the export will use

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>

* Download a video export's images through the capture's own client

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>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
```

## Parents

- [9adbbe24beb3a4b88b824432be1e0051b48e1d09](https://git.cafe/dara/icarus/commit/9adbbe24beb3a4b88b824432be1e0051b48e1d09?format=markdown)

[Source at this commit](https://git.cafe/dara/icarus/tree/3cbd0f7c2b024599870ac140e29a02f962666aaa?format=markdown)
