Skip to content

Commit 3b59dd3

Browse files
committed
feat: Enhance Android H.264 streaming support and refactor related components
- Updated StreamQualityLimits struct to be cloneable and public. - Introduced stream_quality_limits_for_payload function to resolve quality limits from payload. - Refactored Android H.264 socket handling to utilize new quality resolution logic. - Renamed native encoder function to encode BGRA frames instead of RGBA. - Removed deprecated RGBA WebRTC transport handling in favor of H.264. - Implemented shared video frame encoding for Android using native H.264 encoder. - Improved error handling and logging for stream quality updates and encoding failures. - Updated SimDeck documentation to reflect changes in Android live viewing capabilities.
1 parent 2b96df9 commit 3b59dd3

17 files changed

Lines changed: 920 additions & 1880 deletions

File tree

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 streaming both iOS simulators and Android emulators
42+
- Supports native H.264 streaming for both iOS simulators and Android emulators
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/rest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ Performance query parameters:
173173
| `GET` | `/api/simulators/{udid}/control` | Alias for input control WebSocket |
174174
| `POST` | `/api/simulators/{udid}/refresh` | Request a fresh frame or keyframe |
175175

176-
For normal clients, copy the browser behavior instead of hand-coding a raw decoder. The UI supports WebRTC first and H.264 WebSocket fallback.
176+
For normal clients, copy the browser behavior instead of hand-coding a raw decoder. The UI supports WebRTC first and H.264 WebSocket fallback. Android emulator IDs use the same endpoints; their H.264 frames are produced from the emulator `-share-vid` display surface, not screenshot polling.
177177

178178
Minimal WebRTC request:
179179

docs/guide/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This is why a long-lived service feels faster than repeatedly calling lower-leve
2626

2727
## Video flow
2828

29-
The browser opens a live stream for the selected device. SimDeck sends fresh frames, drops stale ones when a client falls behind, and lets the browser request refreshes. The UI can use WebRTC or H.264-over-WebSocket fallback depending on browser support and network behavior.
29+
The browser opens a live stream for the selected device. SimDeck sends fresh frames, drops stale ones when a client falls behind, and lets the browser request refreshes. iOS frames come from the native display bridge and are encoded on the Mac; Android frames come from the emulator `-share-vid` shared display surface and are encoded on the Mac. The UI can use WebRTC or H.264-over-WebSocket fallback depending on browser support and network behavior.
3030

3131
Tune this from the user-facing controls or with:
3232

docs/guide/troubleshooting.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ curl http://127.0.0.1:4310/api/metrics
137137

138138
If `frames_dropped_server` keeps climbing, the client or network cannot keep up. Move closer to the host, reduce quality, or switch to software encoding.
139139

140+
For Android emulator streams, SimDeck uses the emulator `-share-vid` shared display surface. If Android video never starts, confirm `adb devices` shows the emulator as `device`, that it has fully booted, and that externally launched emulators were started with `-share-vid`. SimDeck-owned Android boots add the flag automatically.
141+
140142
### Browser cannot establish WebRTC
141143

142144
Force the H.264 WebSocket fallback while testing:

docs/guide/video.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
SimDeck streams live device video to the browser. Local sessions default to high quality. Remote or constrained sessions can trade detail for lower CPU and latency.
44

55
iOS simulator H.264 uses VideoToolbox for hardware encoding and x264 for software encoding.
6+
Android emulator H.264 uses the emulator `-share-vid` display surface. SimDeck reads BGRA frames from the `videmulator<console-port>` shared memory region and encodes them on the Mac, so normal Android live video stays on the native shared display path.
67

78
## When encoding runs
89

9-
SimDeck starts encoding when a browser stream needs H.264 frames. The server
10-
requests an initial keyframe to answer the WebRTC or H.264 WebSocket viewer,
11-
then keeps a shared refresh pump active while frame subscribers exist.
10+
SimDeck starts encoding when a browser stream needs H.264 frames. For iOS, the
11+
server requests an initial keyframe to answer the WebRTC or H.264 WebSocket
12+
viewer, then keeps a shared refresh pump active while frame subscribers exist.
13+
For Android, SimDeck starts emulators with `-share-vid`, maps the shared display
14+
region, and feeds changed BGRA frames into the native host H.264 encoder.
1215

1316
The browser reports whether the page and stream canvas are foreground. When all
1417
known viewers are hidden or the last frame subscriber disconnects, the native
@@ -58,6 +61,12 @@ simdeck service restart --video-codec software
5861
| `hardware` | Dedicated local machines where VideoToolbox hardware H.264 is reliable. |
5962
| `software` | x264 software H.264 for CI, screen recording conflicts, or hardware encoder stalls. |
6063

64+
The codec setting controls iOS simulator host encoding. Android emulator streams
65+
use a dedicated host encoder for shared display frames; set
66+
`SIMDECK_ANDROID_VIDEO_CODEC=hardware` or `software` before starting the service
67+
when you need to override Android's encoder choice. Stream quality controls the
68+
encoded Android frame size.
69+
6170
When multiple simulator streams run at the same time, `auto` keeps one active
6271
stream on the hardware encoder path and routes additional active auto streams to
6372
software encoding. This avoids saturating the shared VideoToolbox hardware

packages/client/src/api/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface SimulatorMetadata {
5050
isBooted: boolean;
5151
android?: {
5252
avdName?: string;
53-
grpcPort?: number;
53+
consolePort?: number;
5454
serial?: string;
5555
};
5656
privateDisplay?: PrivateDisplayInfo;

packages/client/src/features/stream/streamWorkerClient.test.ts

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
buildStreamTarget,
55
initialStreamBackend,
66
preferredStreamBackend,
7-
shouldUseLocalAndroidRgbaWebRtc,
87
} from "./streamWorkerClient";
98

109
describe("streamWorkerClient", () => {
@@ -25,16 +24,16 @@ describe("streamWorkerClient", () => {
2524
expect(preferredStreamBackend(target)).toBe("webrtc");
2625
});
2726

28-
it("treats explicit RGBA transport as a WebRTC backend", () => {
27+
it("ignores unknown stream query parameters", () => {
2928
const previousWindow = globalThis.window;
3029
Object.defineProperty(globalThis, "window", {
3130
configurable: true,
32-
value: { location: { search: "?stream=rgba" } },
31+
value: { location: { search: "?stream=unknown" } },
3332
});
3433

3534
try {
3635
expect(preferredStreamBackend(buildStreamTarget("android:Pixel_8"))).toBe(
37-
"webrtc",
36+
"auto",
3837
);
3938
} finally {
4039
Object.defineProperty(globalThis, "window", {
@@ -62,63 +61,4 @@ describe("streamWorkerClient", () => {
6261
).RTCPeerConnection = previousPeerConnection;
6362
}
6463
});
65-
66-
it("uses RGBA WebRTC transport for local loopback Android streams", () => {
67-
const previousWindow = globalThis.window;
68-
Object.defineProperty(globalThis, "window", {
69-
configurable: true,
70-
value: { location: { hostname: "127.0.0.1", search: "" } },
71-
});
72-
73-
try {
74-
expect(
75-
shouldUseLocalAndroidRgbaWebRtc(
76-
buildStreamTarget("android:Pixel_8", {
77-
platform: "android-emulator",
78-
transport: "auto",
79-
}),
80-
),
81-
).toBe(true);
82-
} finally {
83-
Object.defineProperty(globalThis, "window", {
84-
configurable: true,
85-
value: previousWindow,
86-
});
87-
}
88-
});
89-
90-
it("keeps Android RGBA disabled for h264 or remote streams", () => {
91-
const previousWindow = globalThis.window;
92-
const location = { hostname: "127.0.0.1", search: "?stream=h264" };
93-
Object.defineProperty(globalThis, "window", {
94-
configurable: true,
95-
value: { location },
96-
});
97-
98-
try {
99-
expect(
100-
shouldUseLocalAndroidRgbaWebRtc(
101-
buildStreamTarget("android:Pixel_8", {
102-
platform: "android-emulator",
103-
transport: "auto",
104-
}),
105-
),
106-
).toBe(false);
107-
location.search = "";
108-
expect(
109-
shouldUseLocalAndroidRgbaWebRtc(
110-
buildStreamTarget("android:Pixel_8", {
111-
platform: "android-emulator",
112-
remote: true,
113-
transport: "auto",
114-
}),
115-
),
116-
).toBe(false);
117-
} finally {
118-
Object.defineProperty(globalThis, "window", {
119-
configurable: true,
120-
value: previousWindow,
121-
});
122-
}
123-
});
12464
});

0 commit comments

Comments
 (0)