Skip to content

feat(recording): make iOS export quality configurable#816

Open
ryanda9910 wants to merge 2 commits into
callstack:mainfrom
ryanda9910:feat/configurable-recording-export-quality
Open

feat(recording): make iOS export quality configurable#816
ryanda9910 wants to merge 2 commits into
callstack:mainfrom
ryanda9910:feat/configurable-recording-export-quality

Conversation

@ryanda9910

@ryanda9910 ryanda9910 commented Jun 19, 2026

Copy link
Copy Markdown

What

Adds a configurable iOS recording export quality. A new record start ... --export-quality <medium|high> option selects the AVAssetExportSession preset used whenever an iOS recording is re-encoded during the stop/export path, whether that re-encode is a --quality resize/downscale or a touch-overlay burn-in.

  • medium (default) selects AVAssetExportPresetMediumQuality, keeping the fast, simulator-friendly export.
  • high selects AVAssetExportPresetHighestQuality for evidence, release notes, or debugging visual artifacts.

This wires up the src/core/recording-export-quality.ts enum that was present but not yet consumed.

Why

Closes #568. The recording resize export was hardcoded to AVAssetExportPresetHighestQuality, so teams could not trade export speed against quality. The default now stays optimized for speed, and a higher-quality mode is available on demand through the same recording stop/export path.

How it is wired

  • src/core/recording-export-quality.ts: keeps the existing RECORDING_EXPORT_QUALITIES / DEFAULT_RECORDING_EXPORT_QUALITY and adds a pure recordingExportQualityToPreset() mapping plus an isRecordingExportQuality() guard.
  • src/utils/cli-flags.ts: registers the --export-quality enum flag.
  • src/commands/recording/index.ts: adds the field to the record command surface, CLI reader, allowed flags, and usage/help.
  • src/daemon/handlers/record-trace-recording.ts: validates the flag (rejects unknown values with INVALID_ARGS), stores it on the recording session, and forwards it to the resize step.
  • src/daemon/handlers/record-trace-finalize.ts: forwards the recording's exportQuality into the touch-overlay finalize step.
  • src/recording/overlay.ts: both resizeRecording and overlayRecordingTouches pass --export-quality to their helpers (overlay defaults to DEFAULT_RECORDING_EXPORT_QUALITY when not set).
  • ios-runner/.../recording-resize.swift and ios-runner/.../recording-overlay.swift: both parse --export-quality, default to medium, and select the matching preset (medium falls back to highest when the medium preset is not compatible with the composition).

Enum to preset mapping

--export-quality AVAssetExportPreset
medium (default) AVAssetExportPresetMediumQuality
high AVAssetExportPresetHighestQuality

Capture quality vs export quality

These stay distinct. The existing integer --quality <5-10> capture flag scales render resolution and is unchanged. The new --export-quality <medium|high> only chooses how hard the exporter works to preserve the video when it re-encodes. The docs and help spell out the difference.

Backward compatibility

The default is medium, which matches the speed-optimized default already used by the export paths and the shipped DEFAULT_RECORDING_EXPORT_QUALITY = 'medium'. Callers that pass no flag get byte-identical output to before (verified in the e2e below: the no-flag run matches --export-quality medium exactly). Anyone who wants the highest-quality export can pass --export-quality high.

How tested

  • pnpm test:unit: full unit suite green (2581 passed in the original change; the overlay follow-up adds two more in src/recording/__tests__/overlay.test.ts).
  • New unit tests: enum to preset mapping and guard (src/core/__tests__/recording-export-quality.test.ts), CLI option parsing (src/commands/recording/index.test.ts), resize + overlay argument forwarding incl. default-preserved behavior (src/recording/__tests__/overlay.test.ts), and daemon validation + plumbing (src/daemon/handlers/__tests__/record-trace.test.ts). None require a live simulator.
  • pnpm typecheck, pnpm lint, pnpm format:check: green. swiftc -typecheck on both modified Swift scripts: OK.

End-to-end on the host (real re-encode)

The touch-overlay burn-in and export-preset re-encode are a macOS host post-process, not a simulator-internal step: exportProcessedVideo (src/recording/overlay.ts) compiles recording-overlay.swift and runs it on the host with --input/--output/--events/--export-quality. The simulator only supplies the raw screen recording as input. So the export-quality logic this PR changes can be exercised end to end on the host exactly as production runs it.

I compiled recording-overlay.swift and ran the real binary against a 720x1280 test clip plus a two-tap gesture envelope, once per quality:

run video bitrate output size preset
--export-quality medium 195 kbps 75 KB AVAssetExportPresetMediumQuality
no flag (default) 195 kbps 75 KB medium (byte-identical to above)
--export-quality high 471 kbps (2.4x) 178 KB AVAssetExportPresetHighestQuality

The flag now demonstrably changes the overlay re-encode preset, and the default path is unchanged. Frames from each output (touch dot burned in):

export-quality overlay e2e

A booted-simulator run would only change where the input clip comes from; it would not exercise any additional export-quality code, since that all runs on the host. If CI has a simulator lane, a full device capture is still welcome as a belt-and-suspenders check before merge.

Wire the existing recording-export-quality enum through the record command
down to the Swift export preset. Adds a `--export-quality <medium|high>`
option for iOS recordings that controls the AVAssetExportSession preset used
when a recording is re-encoded.

`medium` stays the default and selects AVAssetExportPresetMediumQuality, which
preserves the fast simulator-friendly export. `high` opts into
AVAssetExportPresetHighestQuality for evidence-grade output. This is separate
from the existing integer `--quality <5-10>` capture flag that scales render
resolution.

Closes callstack#568
@ryanda9910 ryanda9910 marked this pull request as ready for review June 19, 2026 06:17
@thymikee

Copy link
Copy Markdown
Member

Review finding: --export-quality high is not applied to the touch-overlay export path.

The new flag is stored on the recording and forwarded to resizeRecording, but finalizeRecordingOverlay() still calls overlayRecordingTouches() without export quality (src/daemon/handlers/record-trace-finalize.ts:58), and overlayRecordingTouches() still invokes recording-overlay.swift with only --events (src/recording/overlay.ts:128). That Swift helper always chooses AVAssetExportPresetMediumQuality when compatible (ios-runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift:150).

This means agent-device record start out.mp4 --export-quality high has no effect when the stop path re-encodes only to burn in touch overlays, even though the docs now say the flag controls the iOS export preset whenever a recording is re-encoded (website/docs/docs/commands.md:795) and issue #568 asks for a higher-quality mode wired through the same stop/export path. Please thread exportQuality through finalizeRecordingOverlay/overlayRecordingTouches and add it to recording-overlay.swift, or narrow the docs/help to say the flag only affects resize/downscale exports.

The --export-quality flag was only wired into the resize export path. The
touch-overlay re-encode (finalizeRecordingOverlay -> overlayRecordingTouches ->
recording-overlay.swift) ignored it and always picked AVAssetExportPresetMediumQuality,
so record stop with --export-quality high had no effect when the stop path
re-encodes only to burn in touch overlays.

Thread the recording's exportQuality through finalizeRecordingOverlay and
overlayRecordingTouches, pass it as --export-quality to recording-overlay.swift,
and resolve the preset there via the same exportPresetName() helper used by
recording-resize.swift. Medium stays the default when the arg is absent, so
behavior is unchanged for callers that do not set it.
@ryanda9910

Copy link
Copy Markdown
Author

Good catch, fixed in 8f6eb35.

I threaded the recording's exportQuality through the overlay path the same way it already flows through resize:

  • finalizeRecordingOverlay() now forwards recording.exportQuality into overlayRecordingTouches() (src/daemon/handlers/record-trace-finalize.ts).
  • overlayRecordingTouches() passes it as --export-quality to the Swift helper (src/recording/overlay.ts), defaulting to medium when unset.
  • recording-overlay.swift now parses --export-quality and resolves the preset via an exportPresetName() helper copied from recording-resize.swift, replacing the hardcoded AVAssetExportPresetMediumQuality. Medium stays the default so callers that do not set the flag are unchanged.

Added overlay unit tests mirroring the resize ones (default medium + forwarded high), and tightened the docs note so it reads "whenever a recording is re-encoded, whether that is --quality downscaling it or burning in touch overlays on record stop". swiftc -typecheck on the helper is clean, and typecheck/lint/unit tests pass.

@ryanda9910

Copy link
Copy Markdown
Author

Added an end to end check for this to the PR description. Since the overlay burn-in and export-preset re-encode run on the macOS host (exportProcessedVideo compiles recording-overlay.swift and runs it with the args, the simulator only supplies the raw clip), I compiled the helper and ran the real binary against a test clip plus a two-tap gesture envelope, once per quality:

run video bitrate size
medium 195 kbps 75 KB
no flag (default) 195 kbps 75 KB
high 471 kbps 178 KB

So high now genuinely picks the highest-quality preset on the overlay path and the default is unchanged. Screenshot and full table are in the updated description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make recording export quality configurable

2 participants