Skip to content

feat: record-video captures real H.264 streams - #70

Merged
onevcat merged 5 commits into
mainfrom
fix/record-video-h264-followups
Jul 27, 2026
Merged

onevcat merged 5 commits into
mainfrom
fix/record-video-h264-followups

Conversation

@onevcat

@onevcat onevcat commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Supersedes #52 while preserving its complete commit history.

  • Capture Android screenrecord H.264 output directly into MP4 and use idb's native iOS file recorder.
  • Preserve the final Android frame through the actual stop time, including static or near-static recordings.
  • Drain stdout callbacks already in flight before finalizing the Android muxer, preventing a shutdown race from dropping a final H.264 chunk.

Validation

  • make build
  • make test
  • API 36 emulator: a static 6-second recording now produces a 5.380-second MP4 (previously 0.215 seconds).
  • API 36 emulator: dynamic recording produced a valid 6.035-second, 250-frame MP4.
  • iOS Simulator: native recording produced a valid 3.067-second, 91-frame MP4.

subdiox and others added 5 commits July 17, 2026 16:06
…polling

Replace the screenshot-poll-and-re-encode recorder (capped ~8-10 fps) with
native H.264 capture muxed straight into MP4 (passthrough, no re-encode):

- iOS: FBSimulatorVideoStream eager H.264 at a constant --fps (1-60,
  default 30). The elementary stream carries no timestamps, so frames are
  laid out at exactly 1/fps -- the requested rate is honored and playback
  is smooth (uniform 16.7 ms spacing at 60 fps).
- Android: adb screenrecord --output-format=h264 at the device's native
  variable rate, stitched across the API<34 180 s per-invocation limit.
  --fps is ignored there; --quality/--scale map to bitrate/size.

New shared infra under Sources/iOSSimBackend/Util: AnnexBStreamParser
(NAL splitting + access-unit assembly), H264PassthroughRecorder
(AVAssetWriter passthrough; CFR for iOS, host-clock VFR for Android),
H264MuxingPipeline; plus AdbStreamingProcess for incremental adb stdout.
--fps range widened 1-30 -> 1-60 (default 10 -> 30). The legacy
screenshot/screencap recorders are retained as automatic fallbacks.

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

Signed-off-by: yuta.ooka <yuta.ooka@lycorp.co.jp>
… Sendable

Replace every NSLock + `@unchecked Sendable` in the record-video path with
`OSAllocatedUnfairLock<State>`-backed plain `Sendable` classes
(CancellationFlag, OnceFlag, FirstErrorBox, H264StreamRecorder,
H264PassthroughRecorder, H264MuxingPipeline, AdbStreamingProcess).
Non-Sendable writer / subprocess state is confined to the lock, so no type
needs an unchecked escape hatch and no withLockUnchecked is used. (Mutex
would be cleaner still but requires macOS 15; the package targets macOS 14.)

Also wait for the stream's first frame before committing to it: a cold
FBSimulatorVideoStream that attaches but pushes nothing now falls back to
screenshot capture instead of producing an empty recording.

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

Signed-off-by: yuta.ooka <yuta.ooka@lycorp.co.jp>
…ivery guard, partial-file error wording

Fix three issues from @onevcat's review:

1. Android: --quality was silently ignored unless --scale < 1.0, because
   bitrate was only computed from a size that was only detected when
   scaled. Detect display size unconditionally (still only pass --size
   to screenrecord when scale < 1.0) so --bit-rate reaches screenrecord
   at the default scale too. Verified live: `--quality 20` at default
   scale now yields `--bit-rate 19740672` on the device's screenrecord
   command line (previously absent).

2. iOS: CFR mode now detects when the stream delivers fewer frames than
   the requested rate over the actual recording time (cold start /
   encoder still releasing) and warns on stderr, since the muxed file
   would otherwise silently play back faster than real time with no
   signal to the caller.

3. iOS: mid-stream stream errors that surface after the MP4 was already
   finalized now say "partial recording saved to <path>", matching the
   Android branch's wording, so a usable recording isn't discarded just
   because the command exited non-zero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: yuta.ooka <yuta.ooka@lycorp.co.jp>
…264-stream

Resolves the idb migration merged upstream (76639e4d -> 1f6943f8): static
XCFrameworks, FBDeviceControl removed, FutureBridge/BridgeQueues deleted in
favor of native async/await APIs (CompanionUtilities).

Beyond the textual CHANGELOG.md conflict, this required porting
IOSSimRecordVideoCommand.swift off the old FBVideoStream + manual
FBDataConsumer + Annex-B passthrough-muxing integration (which no longer
compiles) onto upstream's new FBSimulator.startRecording(toFile:
configuration:) -- a native in-process file recorder that internally
drives the same FBSimulatorVideoStream in eager H.264 mode and muxes to
.mp4 via its own AVAssetWriter-backed file writer. This is the same
passthrough-muxing architecture this command hand-rolled for the PR;
it's now upstream's own maintained implementation, so the custom
consumer/pipeline wiring for iOS is gone (Android's adb screenrecord
path is untouched and still needs the Annex-B parser/muxer, since idb
doesn't drive Android at all).

Consequence: H264PassthroughRecorder's CFR (frameRate) mode and its
isUnderDelivered under-delivery guard -- added during the earlier PR
review to fix onevcat's finding #2 -- are now dead code, since iOS no
longer does its own PTS layout (idb's native encoder produces correct
timing) and Android is VFR-only. Removed both, along with their unit
tests; a PR follow-up comment will explain this to the reviewer.

Verified: full unit suite green (849 tests; only the 5 pre-existing
unrelated Init-skill failures), e2e RecordVideoTests 6/6 live on a
booted simulator (including the short-grace SIGTERM finalize test),
and a live Android emulator recording under motion (~60 fps, valid
MP4). Repeated rapid back-to-back iOS recordings to probe the
cold-start zero-frames issue found during the original PR review --
did not reproduce against the new idb, consistent with upstream's
video-layer rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: yuta.ooka <yuta.ooka@lycorp.co.jp>
Signed-off-by: onevcat <onevcat@gmail.com>
@onevcat
onevcat merged commit 11c1886 into main Jul 27, 2026
4 checks passed
@onevcat
onevcat deleted the fix/record-video-h264-followups branch July 27, 2026 10:01
thickfive pushed a commit to thickfive/sim-use that referenced this pull request Sep 14, 2026
`stream-video` had no native format on iOS: `mjpeg`/`raw`/`ffmpeg` were a
screenshot-per-frame loop and `bgra` was raw pixels, so the only way to
watch a simulator live was ~4 fps at ~1.9 MB/s. Meanwhile `record-video`
had been driving native H.264 since lycorp-jp#52/lycorp-jp#70 — the capability was already
there, just not wired to stdout.

Both verbs now build one `FBVideoStreamConfiguration.h264Capture(...)` and
differ only in their sink:

  record-video  -> H264MuxingPipeline -> MP4
  stream-video  -> stdout

That muxer's own docs already described it as "shared by the iOS
(FBSimulatorVideoStream) and Android (adb screenrecord) capture paths";
iOS simply never connected to it, using idb's separate in-process file
writer instead. `AnnexBPipelineConsumer` is the missing adapter from idb's
byte-stream consumer protocol to the pipeline.

Measured on a booted iPhone 17 Pro (iOS 27.0), 6 s captures:

  screenshot loop (mjpeg)   24 frames    4.1 fps   11.2 MB
  screenshot loop (PNG)     26 frames    4.5 fps   92.8 MB
  native h264 Annex B      144 frames   24.0 fps    1.33 MB

The one real trade-off: frame timestamps now come from host arrival time
instead of the encoder's sample clock, since Annex B carries no timing.
Across three runs each under real touch input, inter-frame jitter measured
6.0-6.8 ms on the new path against 2.7-3.1 ms on the old — both far inside
a 33 ms frame at 30 fps, and the `--fps` constant-rate and 100 ms-grace
SIGTERM E2E tests both still pass.

fmp4 transport was evaluated first and rejected: idb's FBFMP4FrameWriter
emits live fragments with no `sidx`/`mfra`, which AVFoundation reads as
zero frames, so a recording would have been unplayable in QuickTime and
untranscodable to GIF. Annex B keeps the MP4 a regular non-fragmented file.

Verified live: both `stream-video --format h264` surfaces plus the
top-level route on an iOS UDID, `ffmpeg -f h264` remux, `bgra`
non-regression, the GIF path, and the full iOS stream-video and
record-video E2E suites.

Refs lycorp-jp#132

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: onevcat <onevcat@gmail.com>
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.

2 participants