Export your video compositions as MP4 files with H.264 encoding, progress events, and configurable quality and resolution.
MP4 is the most widely supported video format, using H.264 encoding for efficient compression. CE.SDK renders frames, encodes them with H.264, and muxes audio into an MP4 container. The export runs on the engine’s background worker so the main thread stays responsive.
This guide covers exporting a page to MP4, observing progress, cancelling an in-flight export, configuring resolution and quality, and exporting a partial timeline range.
Export to MP4#
Call engine.block.exportVideo(_:mimeType:options:) with a page block to export it as an MP4 video. The call returns an AsyncThrowingStream<VideoExport, Error> that yields .progress events while encoding and a final .finished(video:) event that carries the encoded Blob.
let videoStream = try await engine.block.exportVideo(page, mimeType: .mp4)for try await event in videoStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video.mp4")) }}MIMEType.mp4 is the default — pass it explicitly to make the output format obvious to readers of the call site. Once the stream finishes, write the resulting Blob to disk with Blob.write(to:).
Tracking Export Progress#
.progress events arrive throughout the export with three counters: rendered frames (frames pulled from the scene), encoded frames (frames already pushed through the H.264 encoder), and the total frame count. The encoded count is the most useful signal for a user-facing progress bar because it tracks the slower stage.
let progressStream = try await engine.block.exportVideo(page, mimeType: .mp4)for try await event in progressStream { switch event { case let .progress(rendered, encoded, total): let percent = total > 0 ? Int(Double(encoded) / Double(total) * 100) : 0 print("Export \(percent)% — encoded \(encoded)/\(total) (rendered \(rendered))") case let .finished(video: blob): try blob.write(to: exportsDirectory.appendingPathComponent("progress.mp4")) }}Cancelling an Export#
Wrap the export in a Task so a holder can call cancel() to abort it. Each loop iteration calls try Task.checkCancellation() first, so the loop exits as soon as cancellation is requested and the engine tears down the worker.
let exportTask = Task { () -> Blob in let stream = try await engine.block.exportVideo(page, mimeType: .mp4) for try await event in stream { try Task.checkCancellation() if case let .finished(video: blob) = event { return blob } } throw CancellationError()}// Call exportTask.cancel() from another task to abort the export.let exportedBlob = try await exportTask.valuetry exportedBlob.write(to: exportsDirectory.appendingPathComponent("cancellable.mp4"))Hold a reference to the Task value and call cancel() from your UI (for example, when a user taps a Cancel button). The try Task.checkCancellation() call surfaces a CancellationError through exportTask.value instead of returning a partial result.
Configure Video Encoding#
Pass a VideoExportOptions value to control quality, file size, and device compatibility.
Resolution and Framerate#
Set targetWidth, targetHeight, and framerate to control output dimensions and smoothness.
let resolutionOptions = VideoExportOptions( framerate: 30, targetWidth: 1920, targetHeight: 1080,)let resolutionStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: resolutionOptions)for try await event in resolutionStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-1080p.mp4")) }}If only one of targetWidth or targetHeight is non-zero, the other is computed to preserve the source aspect ratio. Without framerate, the video uses the scene’s scene/frameRate (30 by default).
H.264 Profile and Quality#
The h264Profile option controls encoding quality and device compatibility:
.baseline(66): Maximum compatibility, lower compression..main(77): Balanced quality and compatibility (default)..extended(88): Extended feature set, less common..high(100): Best compression, supported on all modern iOS devices.
h264Level accepts the level multiplied by ten — pass 52 for level 5.2. videoBitrate accepts an explicit value in bits per second, or one of the named modes: VideoBitrate.system (0, the default) lets the platform encoder (VideoToolbox) choose, and VideoBitrate.auto (-1) picks a bounded value based on resolution and framerate.
let qualityOptions = VideoExportOptions( h264Profile: .high, h264Level: 52, videoBitrate: 8_000_000,)let qualityStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: qualityOptions)for try await event in qualityStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-high.mp4")) }}The example above sets videoBitrate: 8_000_000 (8 Mbps), a reasonable target for high-quality 1080p H.264 footage. As a rough guide, scale the bitrate with the pixel count and motion complexity: ~5 Mbps for 720p, 8–12 Mbps for 1080p, and 20–40 Mbps for 4K.
Export a Partial Timeline#
Use timeOffset and duration to export a specific segment without modifying the scene. Both values are in seconds, relative to the page’s timeline.
let partialOptions = VideoExportOptions( timeOffset: 1, duration: 2,)let partialStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: partialOptions)for try await event in partialStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-clip.mp4")) }}A duration of 0 defaults to the full duration of the exported page (the engine does not subtract timeOffset).
All MP4 Export Options#
| Option | Type | Default | Description |
|---|---|---|---|
h264Profile |
H264Profile |
.main |
Encoder profile: .baseline (66), .main (77), .extended (88), .high (100). |
h264Level |
Int32 |
52 |
Encoding level (multiply desired level by 10, e.g., 52 for level 5.2). |
videoBitrate |
Int32 |
VideoBitrate.system |
Video bitrate in bits/second, or a named mode. VideoBitrate.system (0, the default) lets VideoToolbox choose; VideoBitrate.auto (-1) picks a bounded, resolution-aware bitrate. A positive number sets an explicit bitrate (maximum determined by profile and level). |
audioBitrate |
Int32 |
0 (auto) |
Audio bitrate in bits/second. 0 defaults to 128 kbps for stereo AAC. |
framerate |
Float? |
nil |
Target framerate in Hz, greater than 0 and at most 240. nil uses the scene’s scene/frameRate. |
targetWidth |
Float |
0 |
Output width in pixels. 0 keeps the page’s natural width. |
targetHeight |
Float |
0 |
Output height in pixels. 0 keeps the page’s natural height. |
timeOffset |
Double |
0 |
Start time offset in seconds. |
duration |
Double |
page length | Video duration in seconds. 0 defaults to the duration of the exported page. |
allowTextOverhang |
Bool |
false |
Include text bounding boxes that account for glyph overhangs. |
API Reference#
| Method | Description |
|---|---|
engine.block.exportVideo(_:mimeType:options:) |
Export a page block as MP4 video. Returns an AsyncThrowingStream<VideoExport, Error>. |
Next Steps#
- Export Overview - Compare all supported export formats
- Export Size Limits - Check device limits before exporting large videos
- Export Audio - Export audio tracks separately
- Partial Export - Export specific blocks or timeline segments
Set a Video Cover#
By default, MP4 exports include a cover from the page playback position when
export starts. Omit coverTimeOffset or pass nil to use this position. For a
partial export, playback stays relative to the page timeline. A position outside
the exported segment uses its first or last frame.
Set coverTimeOffset to override the default in seconds from the beginning of the
exported video. It must be finite, at least 0, and less than the export duration;
invalid values fail the export. For a partial export, this offset is relative to
the exported segment, rather than the page timeline.
The engine embeds the exported frame containing that time as a PNG in the MP4
covr metadata. Within the exported range, a time between frame boundaries
selects the earlier frame, matching playback rather than advancing to the next frame.
The cover preserves the exported aspect ratio, fits within 512 × 512 pixels, and
is never upscaled. Transparent areas use black, matching video playback.
Displaying that cover depends on the player or file browser.
// Override current playback with a cover from the exported video start.let coverOptions = VideoExportOptions(coverTimeOffset: 0)let coverStream = try await engine.block.exportVideo(page, mimeType: .mp4, options: coverOptions)for try await event in coverStream { if case let .finished(video: blob) = event { try blob.write(to: exportsDirectory.appendingPathComponent("video-cover.mp4")) }}