Skip to content

feat: Android support for stream-video + top-level cross-platform verb (#78) - #80

Merged
onevcat merged 2 commits into
mainfrom
feat/android-stream-video
Jul 29, 2026
Merged

onevcat merged 2 commits into
mainfrom
feat/android-stream-video

Conversation

@onevcat

@onevcat onevcat commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #78 on top of the #79 refactor (stacked PR — merge #79 first; GitHub will retarget this to main when the base branch is deleted).

stream-video becomes a genuine cross-platform verb with the standard three-surface layout:

  • sim-use android stream-video --format h264 — native adb exec-out screenrecord --output-format=h264 - passthrough to stdout: variable frame rate, cheap, high quality. screenrecord's per-invocation time limit is papered over by restarting it mid-stream (the record-video segment loop, minus the muxer); each new segment re-emits SPS/PPS, which ffplay/ffmpeg accept.
  • mjpeg / raw / ffmpeg — a screencap-per-frame JPEG loop (~7–8 FPS ceiling) with byte-identical on-the-wire framing to the iOS formats, so existing consumers work unchanged.
  • Top-level sim-use stream-video routes by UDID shape. bgra stays iOS-only, h264 is Android-only for now (iOS H.264 passthrough is a separate follow-up); the mismatch cases fail with a pointer to the right alternative. The 0.5.x agent-typo redirect shim drops stream-video now that the verb is really top-level (caught live — the shim intercepted the new verb before ArgumentParser ever saw it).

Supporting pieces:

  • Consumer-hangup handling: streaming writes go through a POSIX-write stdout sink that ignores SIGPIPE and treats a closed consumer end (ffplay quit, head -c done) as an orderly end-of-stream — summary + exit 0 instead of a crash.
  • SIM_USE_SCREENRECORD_TIME_LIMIT=<seconds> (debug): forces short screenrecord segments so the restart path can be exercised on API ≥ 34 devices, where --time-limit 0 never restarts naturally. Applies to Android record-video and stream-video alike.
  • Shared streaming flag validation (VideoRecordingOptions.validateStreaming) across all three surfaces.

Definition of done (from #78)

  • --format h264 plays live against an emulator, surviving a screenrecord segment restart — verified via the forced-segment E2E test and an ffmpeg live-decode of the top-level stream (headless ffplay proxy; ffplay one-liner documented in the README)
  • Physical Android device — none attached during development; emulator-verified only, needs a follow-up check
  • mjpeg works with the same consumer contract as iOS (multipart framing; raw keeps the 4-byte length prefix)
  • README updated (iOS-only verb list shrinks, ffplay example added); CHANGELOG entry under Unreleased; SKILL.md does not enumerate these verbs, so no change there

Verification

  • make build + make test: 1221 tests green, zero warnings.
  • New AndroidStreamVideoTests live E2E suite (registered in test-runner-android.sh), all 5 green against emulator-5554: h264 Annex B smoke, mjpeg framing, raw length prefix, forced segment restart, consumer hangup.
  • Top-level routing live-checked on both platforms: Android h264 decoded by ffmpeg; iOS mjpeg streamed 16 frames from a booted iPhone 17 Pro simulator; h264-on-iOS and the ios stream-video Android-UDID redirect produce the intended errors.

🤖 Generated with Claude Code

onevcat added 2 commits July 29, 2026 11:04
Closes #78.

AndroidStreamVideoCommand serves two engines behind one flag surface:
--format h264 pipes `adb exec-out screenrecord --output-format=h264 -`
byte-for-byte to stdout (VFR, segment-restart loop from record-video
minus the muxer; each new segment re-emits SPS/PPS, which ffplay/ffmpeg
accept mid-stream), while mjpeg/raw/ffmpeg run a screencap JPEG loop
with byte-identical framing to the iOS formats.

The new top-level StreamVideo forwarder routes by UDID shape: bgra stays
iOS-only, h264 is Android-only for now (iOS passthrough is a separate
follow-up), and mismatches fail with a pointer to the right alternative.
The 0.5.x agent-typo redirect shim drops stream-video from its list now
that the verb is genuinely top-level.

Streaming writes go through a POSIX-write stdout sink that ignores
SIGPIPE and reports consumer hangup (ffplay quit, `head -c` done) as an
orderly end-of-stream instead of crashing mid-summary.

SIM_USE_SCREENRECORD_TIME_LIMIT=<seconds> (debug) forces short
screenrecord segments so the restart path is exercisable on API >= 34
devices, where `--time-limit 0` would otherwise never restart; it
applies to Android record-video and stream-video alike.

Verified: 1221 unit tests green; live AndroidStreamVideoTests E2E suite
(5 tests: h264 Annex B smoke, mjpeg framing, raw length prefix, forced
segment restart, consumer hangup) green against emulator-5554; ffmpeg
decodes the live top-level h264 stream; iOS mjpeg via top-level streams
16 frames from a booted simulator.

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

Signed-off-by: onevcat <onevcat@gmail.com>
…enrecord exits, sync skill docs

Three review findings:

1. --json corrupted the stream (P1): the video bytes own stdout, and the
   generic runner appended the JSON summary envelope to the same stream
   after execute(). All three stream-video surfaces (top-level, ios,
   android) now reject --json at validation time with a targeted error;
   the run summary stays on stderr. Pinned by a parse-level unit test.

2. Blind restart on abnormal screenrecord exit (P2): both segment loops
   restarted whenever a segment had produced bytes, so a screenrecord
   that repeatedly died mid-segment (dead encoder/adb) looped forever.
   Restart now requires a clean exit 0 (the time-limit case); a non-zero
   exit aborts with screenrecord's stderr — record-video still finalizes
   and keeps the partial MP4.

3. Bundled skill cheatsheet still said stream-video was iOS-only (P3):
   the namespace table and the video section now show the cross-platform
   verb and the Android h264/ffplay one-liner; stale five-verb iOS-only
   comments in IOSSimCommand and Command+BatchConvertible updated, and
   IOSSimStreamVideoCommand moved into the cross-platform registration
   group.

Verified: 1222 unit tests green; live AndroidStreamVideoTests suite
green post-change (forced segment restart still restarts on exit 0);
--json rejection exercised on all three surfaces against a live
emulator and simulator.

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

Signed-off-by: onevcat <onevcat@gmail.com>
Base automatically changed from refactor/extract-simusevideo to main July 29, 2026 02:44
@onevcat
onevcat merged commit 5e3e373 into main Jul 29, 2026
1 check passed
@onevcat
onevcat deleted the feat/android-stream-video branch July 29, 2026 02:44
angelmic pushed a commit to angelmic/sim-use that referenced this pull request Jul 29, 2026
…latforms + unit suites

Pre-release audit of the video stack found the pure-logic core well
covered (AnnexBStreamParser 98.8%, H264MuxingPipeline 89.0%,
H264PassthroughRecorder 91.8%) but three blind spots:

E2E: screenshot had no live suite on either platform, and Android
record-video had none (stream-video landed with one in lycorp-jp#80). Added:

- ScreenshotTests (iOS runner): PNG-magic file capture, --json envelope
  whose path is verified on disk, directory-target stamping.
- AndroidScreenshotTests: bridge capture to file, `--output -` raw PNG
  on stdout, top-level serial routing.
- AndroidRecordVideoTests: AVAsset-loadable MP4 with a video track from
  both the android subcommand and the top-level route, plus a forced
  segment restart (SIM_USE_SCREENRECORD_TIME_LIMIT=2) that must still
  finalize one continuous MP4.

Unit: VideoOutputFile was at 0% despite being pure path logic — now
95.7% (default stamping, tilde expansion, directory targets, stale-file
replacement, intermediate dirs). VideoRecordingOptions boundary pins
(93.8%), VideoFrameUtilities frame processing incl. the byte-for-byte
pass-through fast path (the scale path renders via NSImage.lockFocus at
the host's backing scale factor, so tests pin re-encode + aspect ratio,
not absolute pixels), and ProcessControl primitives (66.2%; SignalObserver
left to the live SIGTERM E2E paths).

H264StreamRecorder's AVAssetWriter encoder body stays E2E-only: it is
reachable only through the screencap/screenshot fallbacks, which cannot
be forced without breaking screenrecord.

1252 unit tests green; all 12 new/E2E cases verified live against a
booted iPhone 17 Pro simulator and emulator-5554.

Co-Authored-By: Claude Fable 5 <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.

1 participant