Skip to content

Commit 0fb5775

Browse files
committed
feat: stream Android emulators on Windows and Linux with OpenH264
Non-macOS builds link native_stubs.c instead of the macOS simulator bridge, so the Android emulator WebRTC source had no H.264 encoder and every stream failed with the stub error. Frame capture (emulator gRPC screenshots) and the WebRTC transport were already cross-platform Rust; only the encoder was missing. Server: - Add transport/software_h264.rs: an OpenH264 encoder (openh264 crate, compiled from source, BSD) that turns RGBA/BGRA emulator frames into Annex B baseline H.264. Bitrate follows the macOS budget (bits per pixel with a minimum), keyframes honor RTCP requests, and the encoder rebuilds on size or quality changes. Unit tests cover the budget math, frame validation, keyframe forcing, and reinitialization. - Route the Android WebRTC source through it on non-macOS builds; macOS keeps the native VideoToolbox/x264 path behind the C ABI. Frame publishing is shared between both paths. - Reframe platform.rs around iOS simulator support: liveVideo now reports supported: true everywhere with encoder ("native" or "openh264"), androidEmulator, iosSimulator, and an iosSimulatorReason off macOS. WebRTC offers for iOS simulators on non-macOS answer 501; Android offers proceed. - CLI banner note now says Android streams with software H.264 and iOS simulators require macOS. CI: add a Linux and Windows cargo clippy/test job so the OpenH264 module and the native stubs are linted and tested where they are actually compiled. Docs: per-platform encoder table, health/REST field docs, troubleshooting entries for the iOS message and slow software encoding, AGENTS pointer. Cargo.lock is not updated here (no Rust toolchain on the authoring machine); the first cargo build adds the openh264 entries.
1 parent fbf3d03 commit 0fb5775

19 files changed

Lines changed: 818 additions & 129 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,40 @@ jobs:
5050
- name: Rust unit tests
5151
run: cargo test --manifest-path packages/server/Cargo.toml
5252

53+
rust-non-macos:
54+
name: Rust unit tests (${{ matrix.os }})
55+
runs-on: ${{ matrix.os }}
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
os: [ubuntu-latest, windows-latest]
60+
61+
steps:
62+
- uses: actions/checkout@v4
63+
64+
- uses: dtolnay/rust-toolchain@stable
65+
with:
66+
components: clippy
67+
68+
- name: Cache Rust build outputs
69+
uses: actions/cache@v4
70+
with:
71+
path: |
72+
~/.cargo/git
73+
~/.cargo/registry
74+
packages/server/target
75+
key: rust-${{ runner.os }}-${{ hashFiles('packages/server/Cargo.lock', 'packages/server/Cargo.toml', 'packages/server/build.rs', 'packages/server/src/**/*.rs', 'packages/server/native_stubs.c') }}
76+
restore-keys: |
77+
rust-${{ runner.os }}-
78+
79+
# The macOS job cannot compile the OpenH264 software encoder or the
80+
# native stubs, so lint and test them where they are actually built.
81+
- name: Clippy
82+
run: cargo clippy --manifest-path packages/server/Cargo.toml --all-targets -- -D warnings
83+
84+
- name: Rust unit tests
85+
run: cargo test --manifest-path packages/server/Cargo.toml
86+
5387
client:
5488
name: Client lint, build, and tests
5589
runs-on: ubuntu-latest

‎AGENTS.md‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,18 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
2121
- `packages/server/src/transport/webrtc.rs`
2222
Exposes the H.264 WebRTC offer/answer endpoint for browser live video.
2323
- `packages/server/src/platform.rs`
24-
Single source of truth for host platform capabilities. Live H.264 video
25-
(iOS simulator and Android emulator) exists only in the macOS build; the
26-
Windows and Linux builds compile `packages/server/native_stubs.c` in place
27-
of the native bridge. Health, stream-quality, the WebRTC offer error, the
28-
CLI banner, and the browser client all read this module's `liveVideo`
29-
capability instead of hard-coding platform checks.
24+
Single source of truth for host platform capabilities. iOS simulators exist
25+
only in the macOS build; the Windows and Linux builds compile
26+
`packages/server/native_stubs.c` in place of the native bridge. Health,
27+
stream-quality, the WebRTC offer error, the CLI banner, and the browser
28+
client all read this module's `liveVideo` capability instead of hard-coding
29+
platform checks.
30+
- `packages/server/src/transport/software_h264.rs`
31+
OpenH264 software encoder (the `openh264` crate, compiled from source) used
32+
for Android emulator WebRTC streams on non-macOS builds. It emits Annex B
33+
baseline H.264 so `transport/webrtc.rs` packetizes it exactly like the macOS
34+
native encoder output. macOS keeps encoding Android frames through
35+
`XCWH264Encoder` behind the C ABI.
3036
- `packages/server/src/webkit.rs`
3137
Discovers simulator WebKit Remote Inspector targets and bridges WebInspectorUI
3238
WebSocket traffic to the simulator `webinspectord` binary-plist socket.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ view inside the editor.
3939

4040
## Features
4141

42-
- Supports native H.264 streaming for both iOS simulators and Android emulators (live video requires the macOS build; the Windows and Linux CLIs manage Android emulators without a browser stream)
42+
- Supports native H.264 streaming for both iOS simulators and Android emulators (iOS simulators need the macOS build; Windows and Linux stream Android emulators with a built-in OpenH264 software encoder)
4343
- Full simulator control & inspection using private iOS accessibility APIs and Android UIAutomator - available using `simdeck` CLI
4444
- Real-time screen `describe` command using accessibility view tree - available in token-efficient format for agents
4545
- Profiling built-in: CPU, memory, disk writes, network throughput, hang signals, and stack sampling

‎docs/api/health.md‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ Example:
2222
"timestamp": 1714094761.234,
2323
"videoCodec": "auto",
2424
"hostOs": "macos",
25-
"liveVideo": { "supported": true, "requires": "macos" },
25+
"liveVideo": {
26+
"supported": true,
27+
"encoder": "native",
28+
"iosSimulator": true,
29+
"androidEmulator": true
30+
},
2631
"androidGpu": "host",
2732
"lowLatency": false,
2833
"realtimeStream": true,
@@ -50,16 +55,19 @@ Important fields:
5055
| `serverKind` | `launchAgent` or `standalone` |
5156
| `videoCodec` | Requested codec mode: `auto`, `hardware`, or `software` |
5257
| `hostOs` | Server build target: `macos`, `windows`, or `linux` |
53-
| `liveVideo` | Whether this build can encode the live H.264 stream |
58+
| `liveVideo` | Live stream encoder and per-platform device support |
5459
| `androidGpu` | Android emulator renderer mode for SimDeck-owned boots |
5560
| `streamQuality` | Active stream profile and limits |
5661
| `webRtc` | ICE settings the browser should use |
5762

58-
`liveVideo` is `{ "supported": true, "requires": "macos" }` on macOS. Windows
59-
and Linux builds return `supported: false` plus a `reason` string, because live
60-
H.264 encoding for both iOS simulators and Android emulators lives in the macOS
61-
native bridge. Clients should show that reason instead of opening a WebRTC
62-
offer; the offer endpoint answers `501 Not Implemented` with the same message.
63+
`liveVideo.encoder` is `native` on macOS (VideoToolbox or x264 through the
64+
simulator bridge) and `openh264` on Windows and Linux, where Android emulator
65+
frames are encoded in software. `androidEmulator` is `true` everywhere.
66+
`iosSimulator` is `true` only on macOS; other builds add an
67+
`iosSimulatorReason` string, and the WebRTC offer endpoint answers iOS
68+
simulator offers there with `501 Not Implemented` and the same message.
69+
`supported` is `true` for every shipped build; a client should treat
70+
`supported: false` plus `reason` as "do not open a WebRTC offer".
6371
`GET /api/stream-quality` includes the same `liveVideo` block.
6472

6573
When auth is required, the `401` JSON body still includes `serverId`, `advertiseHost`, `hostId`, `hostName`, `httpPort`, and `serverKind` so native clients can group endpoints before pairing.

‎docs/api/rest.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ curl -X POST \
5353
| `POST` | `/api/stream-quality` | Update stream quality settings |
5454

5555
See [Health and metrics](/api/health) for details. Both `/api/health` and
56-
`/api/stream-quality` include a `liveVideo` block; check `liveVideo.supported`
57-
before opening a WebRTC offer, because Windows and Linux builds cannot encode
58-
the live stream and answer offers with `501` and the `liveVideo.reason` text.
56+
`/api/stream-quality` include a `liveVideo` block. Android emulators stream on
57+
every platform (`encoder` is `native` on macOS and `openh264` elsewhere); check
58+
`liveVideo.iosSimulator` before opening an iOS simulator WebRTC offer, because
59+
Windows and Linux builds answer those with `501` and `iosSimulatorReason`.
5960

6061
## Devices
6162

‎docs/guide/installation.md‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,22 @@
88

99
## Platform support
1010

11-
The npm package installs a native CLI for macOS, Windows, and Linux, but only
12-
the macOS build includes the native simulator bridge and H.264 encoder.
11+
The npm package installs a native CLI for macOS, Windows, and Linux. Only the
12+
macOS build includes the private simulator bridge, so iOS simulators need a
13+
Mac. Android emulators work everywhere, including the live browser stream.
1314

14-
| Capability | macOS | Windows / Linux |
15-
| -------------------------------------------------- | ----- | ----------------------- |
16-
| iOS simulator control, inspection, and streaming | Yes | No |
17-
| Android emulator control and inspection | Yes | Yes |
18-
| Live H.264 browser stream (iOS and Android) | Yes | No |
19-
| `--video-codec`, stream quality, and encoder menus | Yes | Reported as unavailable |
15+
| Capability | macOS | Windows / Linux |
16+
| ------------------------------------------------ | -------------------- | ---------------------- |
17+
| iOS simulator control, inspection, and streaming | Yes | No |
18+
| Android emulator control and inspection | Yes | Yes |
19+
| Android emulator live H.264 browser stream | VideoToolbox or x264 | OpenH264 software |
20+
| `--video-codec hardware` | VideoToolbox | Falls back to software |
2021

2122
On Windows and Linux the CLI prints a `Live video:` note after the service
22-
URLs, `GET /api/health` and `GET /api/stream-quality` report
23-
`liveVideo.supported: false` with the reason, the browser shows that reason in
24-
place of the device screen, and the WebRTC offer endpoint answers `501` with
25-
the same message. See [Video and streaming](./video.md) for the encoder details.
23+
URLs, and `GET /api/health` reports `liveVideo.encoder: "openh264"` with
24+
`liveVideo.iosSimulator: false`. Selecting an iOS simulator through the API
25+
there returns `501` with the same explanation. See
26+
[Video and streaming](./video.md) for the encoder details.
2627

2728
Check Xcode selection if you have multiple installs:
2829

‎docs/guide/troubleshooting.md‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,32 @@ Android IDs in SimDeck use `android:<avd-name>`.
107107

108108
## Stream is black or stuck
109109

110-
### Live video requires macOS
110+
### iOS simulators require macOS
111111

112112
```text
113-
Live H.264 video streaming requires macOS. This SimDeck build for Windows can manage devices but does not include the native H.264 encoder...
113+
iOS simulators require macOS. This SimDeck build for Windows can boot, control, and stream Android emulators, but the iOS simulator bridge is only available on macOS.
114114
```
115115

116-
This is expected on the Windows and Linux builds, for both iOS simulators and
117-
Android emulators. Those builds link a stub instead of the macOS native bridge
118-
that owns VideoToolbox and x264 encoding, so the browser stream, the encoder
119-
menu, and `--video-codec` cannot work there. Device control, `describe`,
120-
screenshots, and the inspectors still run. Use a Mac to see the live screen, or
121-
pair a Windows or Linux browser with a SimDeck service running on a Mac. Check
122-
`liveVideo.supported` in `GET /api/health` when scripting against the service.
116+
This is expected on the Windows and Linux builds. They link a stub instead of
117+
the macOS native bridge that owns iOS simulator control and the VideoToolbox
118+
and x264 encoders. Android emulators still boot, stream, and accept input on
119+
those hosts through the built-in OpenH264 software encoder. Use a Mac for iOS
120+
simulators, or pair a Windows or Linux browser with a SimDeck service running on
121+
a Mac. Check `liveVideo.iosSimulator` in `GET /api/health` when scripting
122+
against the service.
123+
124+
### Android stream is slow on Windows or Linux
125+
126+
Those builds encode with OpenH264 in software. Lower the encoded size first,
127+
then the frame rate:
128+
129+
```sh
130+
simdeck service restart --stream-quality low
131+
```
132+
133+
`GET /api/metrics` lists `androidEncoders[].encoder.native` with
134+
`latestEncodeLatencyUs` and `skippedFrames`; if encode latency stays above the
135+
frame interval, the host CPU cannot keep up with the selected profile.
123136

124137
### Timed out waiting for the first frame
125138

‎docs/guide/video.md‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,26 @@ SimDeck streams live device video to the browser. Local iOS sessions default to
55
iOS simulator H.264 uses VideoToolbox for hardware encoding and x264 for software encoding.
66
Android emulator H.264 uses the emulator gRPC `streamScreenshot` API when SimDeck owns the boot. SimDeck receives raw RGBA frames, pads odd dimensions for H.264, and encodes them on the Mac. If the gRPC endpoint is unavailable, SimDeck falls back to the emulator `-share-vid` display surface and reads BGRA frames from the `videmulator<console-port>` shared memory region.
77

8-
## Platform requirement
9-
10-
Live video needs the macOS build. Both encoders above live in the macOS native
11-
bridge, so the Windows and Linux CLIs link a stub instead and cannot stream iOS
12-
simulators or Android emulators to the browser. Those builds still manage
13-
Android emulators, but `--video-codec`, stream quality, and the encoder menu
14-
have no effect there. The service reports this through `liveVideo` in
15-
`GET /api/health` and `GET /api/stream-quality`, prints a `Live video:` note
16-
when it starts, and answers WebRTC offers with `501 Not Implemented` and the
17-
same explanation. The browser client hides the retry loop and shows that
18-
message in place of the device screen.
8+
## Encoders per platform
9+
10+
| Host | iOS simulator stream | Android emulator stream |
11+
| --------------- | ----------------------------- | ---------------------------------------- |
12+
| macOS | VideoToolbox hardware or x264 | VideoToolbox hardware or x264 |
13+
| Windows / Linux | Not available | OpenH264 software, built into the binary |
14+
15+
Both macOS encoders live in the macOS native bridge, which also owns iOS
16+
simulator control. The Windows and Linux CLIs link a stub in its place, so they
17+
cannot drive iOS simulators at all. Android emulator frames on those hosts are
18+
encoded in Rust with Cisco's OpenH264 (baseline profile, Annex B) and go
19+
through the same WebRTC path as on macOS. Stream quality profiles and the
20+
frame-rate and resolution controls apply there too; `--video-codec hardware`
21+
has no hardware encoder to select and behaves like `software`.
22+
23+
The service reports this through `liveVideo` in `GET /api/health` and
24+
`GET /api/stream-quality` (`encoder` is `native` or `openh264`, `iosSimulator`
25+
is `false` off macOS), prints a `Live video:` note when it starts on Windows or
26+
Linux, and answers iOS simulator WebRTC offers there with `501 Not Implemented`
27+
and the same explanation.
1928

2029
## When encoding runs
2130

‎packages/client/src/api/types.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,18 @@ export interface ChromeDevToolsTargetDiscovery {
188188
}
189189

190190
/**
191-
* Whether the server build can encode the live H.264 browser stream. Only the
192-
* macOS build links the native encoder; Windows and Linux builds report
193-
* `supported: false` with a user-facing `reason`.
191+
* What the server build can stream. Every shipped build encodes the live H.264
192+
* browser stream (`native` VideoToolbox/x264 on macOS, `openh264` elsewhere),
193+
* but only macOS can drive iOS simulators; Windows and Linux builds report
194+
* `iosSimulator: false` with a user-facing `iosSimulatorReason`. A server that
195+
* cannot stream at all would report `supported: false` with a `reason`.
194196
*/
195197
export interface LiveVideoCapability {
196198
supported: boolean;
199+
encoder?: string;
200+
iosSimulator?: boolean;
201+
androidEmulator?: boolean;
202+
iosSimulatorReason?: string | null;
197203
requires?: string;
198204
reason?: string | null;
199205
}

‎packages/client/src/app/AppShell.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,9 @@ export function AppShell({
628628
);
629629
const [streamConfigApplyKey, setStreamConfigApplyKey] = useState(0);
630630
const [streamConfigReady, setStreamConfigReady] = useState(false);
631-
// Non-empty when the connected server build cannot encode live video (for
632-
// example the Windows or Linux CLI). The stream stays paused and the reason
633-
// is shown instead of retrying WebRTC offers that can never succeed.
631+
// Non-empty when the connected server reports that it cannot encode live
632+
// video at all. The stream stays paused and the reason is shown instead of
633+
// retrying WebRTC offers that can never succeed.
634634
const [liveVideoUnavailable, setLiveVideoUnavailable] = useState("");
635635
const [touchIndicators, setTouchIndicators] = useState<TouchIndicator[]>([]);
636636
const [selectedSimulatorState, setSelectedSimulatorState] =

0 commit comments

Comments
 (0)