Skip to content

Auto-downgrade to MJPEG when VideoToolbox can't encode H.264#105

Open
EvanBacon wants to merge 3 commits into
mainfrom
codec-encoder-probe
Open

Auto-downgrade to MJPEG when VideoToolbox can't encode H.264#105
EvanBacon wants to merge 3 commits into
mainfrom
codec-encoder-probe

Conversation

@EvanBacon

@EvanBacon EvanBacon commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Why

Virtualized macOS hosts (cloud Macs like Namespace, CI VMs) accept VTCompressionSessionCreate but never actually produce H.264 output. Today the AVCC stream paints its JPEG seed and then sits frozen until the client's no-frame timeout (~4s) downgrades to MJPEG — a poor first-load experience, and the reason cloud users have to know to pass --codec mjpeg.

This makes serve-sim on a cloud Mac just work: the helper detects its own encoder's capability and tells viewers to use MJPEG up front.

How

  • Startup probe (helper): feed a few captured frames through VideoToolbox at launch, independent of any AVCC viewer. First encoded output ⇒ H.264 OK; no output within 3s ⇒ unsupported. Surfaced on /config as h264Supported.
  • 0x05 downgrade envelope: sent to an AVCC viewer on connect (and broadcast to any already-connected viewers) when the encoder is known dead. Empty payload; tag kept in sync between Swift AVCCEnvelope and the TS avcc-codec.
  • Client: the demuxer decodes the downgrade chunk and routes it through the existing onDecoderError → MJPEG fallback, so the switch is immediate. The 4s no-frame timeout stays as a backstop.

--codec remains an explicit override; this only changes the default behavior on hosts that can't encode H.264.

Verification

  • Bare-metal Mac: /config{"h264Supported":true,...}, log [h264] probe: VideoToolbox H.264 encode OK, stream stays on H.264. ✅
  • Typecheck, lint, serve-sim (249) + serve-sim-client (100, incl. new downgrade demuxer test) all green.
  • ⚠️ The VM/false path (cloud Mac → MJPEG) still needs on-device verification; the existing client fallback de-risks it.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added AVCC “downgrade” support (tag 0x05) to detect when H.264 encoding is unavailable and promptly signal a fallback to MJPEG.
    • Introduced startup H.264 capability probing so encoding and viewer behavior switch based on actual host support.
  • Tests

    • Expanded AVCC codec and stream endpoint tests to cover downgrade envelopes, including parsing/early-exit behavior and empty-payload downgrade handling.

…e H.264

Virtualized macOS hosts (cloud Macs, CI VMs) accept VTCompressionSessionCreate
but never produce H.264 output, so the AVCC stream would sit on a frozen seed
until the client's no-frame timeout (~4s) fired. The helper now probes its own
encoder at startup and tells viewers to use MJPEG up front — so `serve-sim` on
a cloud Mac "just works" without needing `--codec mjpeg`.

- Helper: feed a few captured frames through VideoToolbox at startup,
  independent of any AVCC viewer; first encoded output ⇒ H.264 OK, no output
  within 3s ⇒ unsupported. Result surfaced via /config (`h264Supported`).
- New AVCC `0x05` downgrade envelope: sent to an AVCC viewer on connect (and
  broadcast to any already-connected viewers) when the encoder is known dead.
- Client demuxer decodes the downgrade chunk and routes it through the existing
  onDecoderError → MJPEG fallback, so the switch is immediate instead of a 4s
  stall. The no-frame timeout remains as a backstop.

`--codec` stays an explicit override. Verified on bare metal: probe reports
h264Supported:true and the stream stays on H.264. VM (false) path still needs
on-device verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f8a1818f-0b89-4157-8243-6b9881de23e6

📥 Commits

Reviewing files that changed from the base of the PR and between 24af360 and 02e6f36.

📒 Files selected for processing (3)
  • packages/serve-sim/Sources/SimStreamHelper/main.swift
  • packages/serve-sim/bin/serve-sim-bin
  • packages/serve-sim/src/__tests__/avcc-stream-endpoint.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/serve-sim/src/tests/avcc-stream-endpoint.test.ts
  • packages/serve-sim/Sources/SimStreamHelper/main.swift

📝 Walkthrough

Walkthrough

Adds a new 0x05 "downgrade" AVCC tag that signals clients to fall back from H.264 to MJPEG when VideoToolbox encoding is unavailable. The Swift server gains a startup capability probe; ClientManager tracks the probe result and fans out the downgrade envelope. The TypeScript client codec decodes the new tag, and useAvccStream reports decode failure on receipt.

Changes

AVCC H.264 downgrade signal

Layer / File(s) Summary
Wire format and codec protocol definitions
packages/serve-sim/Sources/SimStreamHelper/StreamFormat.swift, packages/serve-sim-client/src/avcc-codec.ts
StreamFormat.swift adds downgradeTag (0x05) and a downgrade() envelope helper. avcc-codec.ts exports AVCC_TAG_DOWNGRADE, extends AvccChunkType with "downgrade", and adds the tag to TAG_TO_TYPE.
Server-side H.264 capability probe
packages/serve-sim/Sources/SimStreamHelper/main.swift
Introduces h264Decided/h264OK flags and probe timing. onEncoded now finalizes the probe on first output. Per-frame scheduling encodes when AVCC viewers are connected or the probe is undecided; forces a keyframe on the first attempt and marks H.264 unsupported after a timeout with no output.
ClientManager h264Supported state and downgrade fan-out
packages/serve-sim/Sources/SimStreamHelper/ClientManager.swift
Adds h264Supported: Bool? cache. screenConfig() conditionally includes the probe result. setH264Supported(false) broadcasts the downgrade envelope and updated screen config. sendInitialAvcc(to:) checks h264SupportedSnapshot() at connect time and either sends the downgrade immediately or seeds normally, gating onAvccClientConnect behind H.264 availability.
Client downgrade handling and codec tests
packages/serve-sim-client/src/simulator/use-avcc-stream.ts, packages/serve-sim-client/src/__tests__/avcc-codec.test.ts, packages/serve-sim/src/__tests__/avcc-stream-endpoint.test.ts
useAvccStream handles the "downgrade" chunk by calling reportDecodeFailure with "server: H.264 encode unavailable". avcc-codec.test.ts adds frame/concat helpers and a new demuxer case asserting seed→downgrade order and zero-length downgrade payload. avcc-stream-endpoint.test.ts recognizes TAG_DOWNGRADE, treats it as a successful outcome, and validates that the downgrade stream does not contain keyframes.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(99, 102, 241, 0.5)
    Note over main.swift,h264Encoder: Startup H.264 probe
  end
  participant main.swift
  participant h264Encoder
  participant ClientManager
  participant AVCCClient as AVCC Client (TypeScript)

  main.swift->>h264Encoder: encode first frame (forced keyframe)
  alt H.264 output received within timeout
    h264Encoder-->>main.swift: onEncoded callback
    main.swift->>ClientManager: setH264Supported(true)
  else Timeout, no output
    main.swift->>ClientManager: setH264Supported(false)
    ClientManager->>AVCCClient: broadcast downgrade envelope (tag 0x05)
  end

  AVCCClient->>AVCCClient: AvccDemuxer parses tag 0x05 → "downgrade"
  AVCCClient->>AVCCClient: useAvccStream reportDecodeFailure("server: H.264 encode unavailable")
  AVCCClient->>AVCCClient: fall back to MJPEG
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • EvanBacon/serve-sim#78: Established the original AVCC demuxing and TAG_TO_TYPE dispatch logic that this PR directly extends with the new 0x05 tag.

Poem

🐰 Hoppin' through the codec stream,
A new tag lands with a silent gleam—
"0x05," the server cries,
"No H.264? Switch to MJPEG ties!"
The probe fires once, then decides the fate,
And the client falls back — never too late. 🎥

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Auto-downgrade to MJPEG when VideoToolbox can't encode H.264' directly and accurately summarizes the main change: implementing automatic codec downgrade detection when H.264 encoding is unavailable, which is the core objective across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codec-encoder-probe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/serve-sim/Sources/SimStreamHelper/main.swift`:
- Around line 65-69: The h264Decided variable and related probe state (h264OK,
h264ProbeStart) are accessed both inside and outside the h264Queue, creating a
data race where line 193 reads h264Decided outside the queue while probe
operations write to it inside the queue. Move all reads of h264Decided, h264OK,
and h264ProbeStart to occur within the h264Queue synchronization context to
ensure thread-safe access. Additionally, relocate the timeout check (comparing
current time against h264ProbeStart and h264ProbeTimeout) to execute before the
h264Encoding backpressure condition is evaluated, so the downgrade decision is
not blocked by encoding backpressure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f4cffe8d-c7f3-47d2-aa35-6cb42cc0d188

📥 Commits

Reviewing files that changed from the base of the PR and between d93d45e and c7e2961.

📒 Files selected for processing (7)
  • packages/serve-sim-client/src/__tests__/avcc-codec.test.ts
  • packages/serve-sim-client/src/avcc-codec.ts
  • packages/serve-sim-client/src/simulator/use-avcc-stream.ts
  • packages/serve-sim/Sources/SimStreamHelper/ClientManager.swift
  • packages/serve-sim/Sources/SimStreamHelper/StreamFormat.swift
  • packages/serve-sim/Sources/SimStreamHelper/main.swift
  • packages/serve-sim/bin/serve-sim-bin

Comment thread packages/serve-sim/Sources/SimStreamHelper/main.swift
On a virtualized runner the helper's startup probe finds no VideoToolbox H.264
and emits the new 0x05 downgrade envelope, so the AVCC endpoint test now sees
tags [seed, downgrade]. Recognize that as the designed H.264-unavailable
outcome (assert valid framing + no keyframe, break early) instead of failing on
an unknown tag or waiting out the 30s soft-pass budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/serve-sim/src/__tests__/avcc-stream-endpoint.test.ts`:
- Around line 113-116: The test prematurely exits when TAG_DOWNGRADE is observed
by breaking immediately, which means it doesn't verify that keyframes don't
appear after the downgrade signal is received. Instead of breaking immediately
upon seeing TAG_DOWNGRADE, modify the logic to continue reading from the stream
to check that no keyframes appear post-downgrade, ensuring the "downgrade stream
does not contain keyframes" guarantee is actually verified rather than assumed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b457675c-2e67-4b6a-af19-582320e64b78

📥 Commits

Reviewing files that changed from the base of the PR and between c7e2961 and 24af360.

📒 Files selected for processing (1)
  • packages/serve-sim/src/__tests__/avcc-stream-endpoint.test.ts

Comment thread packages/serve-sim/src/__tests__/avcc-stream-endpoint.test.ts Outdated
- main.swift: confine all H.264 probe/backpressure state (h264Decided, h264OK,
  h264ProbeStart, h264Encoding, forceKeyframe) to h264Queue. The frame handler
  previously read h264Decided from the capture thread while the queue wrote it —
  a data race. The gating decision now runs inside the queue, and the probe
  timeout is evaluated before the backpressure early-return so a wedged encode
  can't suppress the downgrade decision.
- avcc-stream-endpoint.test.ts: don't break the read loop immediately on the
  downgrade tag; keep reading for a 300ms grace window so a keyframe emitted
  after downgrade (a protocol violation) is actually observed before asserting
  none exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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