Skip to content

Commit 7b275de

Browse files
committed
Stabilize runtime codec switching
1 parent 3c7bd5a commit 7b275de

6 files changed

Lines changed: 97 additions & 119 deletions

File tree

LICENSE

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
Copyright 2026 Dj
1+
Copyright OpenJS Foundation and other contributors, https://openjsf.org
22

3-
Licensed under the Apache License, Version 2.0 (the "License");
4-
you may not use this file except in compliance with the License.
5-
You may obtain a copy of the License at
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
69

7-
http://www.apache.org/licenses/LICENSE-2.0
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
812

9-
Unless required by applicable law or agreed to in writing, software
10-
distributed under the License is distributed on an "AS IS" BASIS,
11-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
See the License for the specific language governing permissions and
13-
limitations under the License.
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

Lines changed: 32 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
# SimDeck
1+
<p align="center">
2+
<img width="180" src="./client/public/simdeck.png">
23

3-
SimDeck is a developer tool built for streamlining mobile app development for coding agents.
4-
Drive iOS Simulator apps from the CLI, browser, and automated tests on macOS.
4+
<h1 align="center">SimDeck</h1>
5+
6+
<p align="center">
7+
SimDeck is a developer tool built for streamlining mobile app development for coding agents.
8+
Drive Simulator from the CLI using agents, browser, and automated tests on macOS.
9+
</p>
10+
</p>
11+
12+
<hr/>
13+
14+
## Try it out
15+
16+
```sh
17+
npx simdeck
18+
```
19+
20+
Install the CLI globally for agentic-use:
521

622
```sh
723
npm i -g simdeck@latest
@@ -11,25 +27,24 @@ After installing the CLI, install the Codex skill so agents know the stable
1127
SimDeck workflow:
1228

1329
```sh
14-
npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
30+
npx skills add NativeScript/SimDeck --skill simdeck -g
1531
```
1632

17-
For VS Code, install the `nativescript.simdeck` extension to open the simulator
33+
For VS Code, install the `nativescript.simdeck-vscode` extension to open the simulator
1834
view inside the editor.
1935

2036
## Features
2137

22-
- WebTransport streaming server in Rust, plus experimental WebRTC media previews, using HEVC or H.264 video
23-
- Simulator control & inspection using private accessibility APIs
38+
- Local simulator video stream over WebTransport or WebRTC, hardware-encoded in full resolution
39+
- Full simulator control & inspection using private accessibility APIs
2440
- CoreSimulator chrome asset rendering for device bezels
25-
- NativeScript and React Native runtime inspector plugins, plus a native UIKit inspector framework for other apps
26-
- Project daemon reuse: normal CLI commands automatically start and reuse one warm native host per project.
27-
- Optional macOS LaunchAgent service for an always-on local SimDeck daemon.
41+
- NativeScript, React Native, UIKit and SwiftUI runtime inspector plugins to view app's view hierarchy live
2842
- `simdeck/test` for fast JS/TS app tests that can query accessibility state and drive simulator controls.
43+
- SimDeck Studio for automatic PR deployments to on-demand simulators
2944

3045
## Documentation
3146

32-
Full documentation lives at [simdeck.nativescript.org](https://simdeck.nativescript.org/), with guides, the CLI reference, the REST API, the WebTransport video pipeline, and the inspector protocols.
47+
Full documentation lives at [simdeck.nativescript.org](https://simdeck.nativescript.org/), with guides, the CLI reference, the REST API, the video pipeline, and the inspector protocols.
3348

3449
## Quick start
3550

@@ -66,32 +81,7 @@ simdeck daemon status
6681
simdeck daemon stop
6782
```
6883

69-
`simdeck daemon` manages the normal per-project warm process. For an always-on
70-
daemon that is available after login, use the macOS user service commands:
71-
72-
```sh
73-
simdeck service on
74-
simdeck service off
75-
```
76-
77-
This uses a LaunchAgent, keeps the server bound to localhost by default, and is
78-
best for agents or editor integrations that should be able to open SimDeck
79-
without first starting a project daemon.
80-
81-
Use software H.264 when macOS screen recording starves the hardware encoder:
82-
83-
```sh
84-
simdeck daemon start --video-codec h264-software
85-
```
86-
87-
The browser reads `/api/health` and automatically selects the WebRTC media
88-
transport for software H.264.
89-
90-
For LAN browser access:
91-
92-
```sh
93-
simdeck ui --bind 0.0.0.0 --advertise-host 192.168.1.50 --open
94-
```
84+
`simdeck daemon` manages the normal per-project warm process.
9585

9686
Restart the CoreSimulator service layer when `simctl` reports a stale service
9787
version or the live display gets stuck before the first frame:
@@ -156,59 +146,6 @@ lower-token hierarchy dumps. Coordinate commands accept screen coordinates from
156146
the accessibility tree by default; pass `--normalized` to send `0.0..1.0`
157147
coordinates directly.
158148

159-
## Daemon
160-
161-
Manage the project daemon explicitly when needed:
162-
163-
```sh
164-
simdeck daemon start
165-
simdeck daemon status
166-
simdeck daemon stop
167-
```
168-
169-
`simdeck daemon` manages the normal per-project warm process. For an always-on
170-
daemon that is available after login, use the macOS user service commands:
171-
172-
```sh
173-
simdeck service on
174-
simdeck service off
175-
```
176-
177-
This uses a LaunchAgent, keeps the server bound to localhost by default, and is
178-
best for agents or editor integrations that should be able to open SimDeck
179-
without first starting a project daemon.
180-
181-
Use software H.264 when macOS screen recording starves the hardware encoder:
182-
183-
```sh
184-
simdeck daemon start --video-codec h264-software
185-
```
186-
187-
For LAN browser access:
188-
189-
```sh
190-
simdeck ui --bind 0.0.0.0 --advertise-host 192.168.1.50 --open
191-
```
192-
193-
Restart the CoreSimulator service layer when `simctl` reports a stale service
194-
version or the live display gets stuck before the first frame:
195-
196-
```sh
197-
simdeck core-simulator restart
198-
```
199-
200-
You can also start or stop the CoreSimulator service layer explicitly:
201-
202-
```sh
203-
simdeck core-simulator start
204-
simdeck core-simulator shutdown
205-
```
206-
207-
The daemon exposes HTTP on the requested port and WebTransport on `port + 1`.
208-
The browser bootstrap comes from `GET /api/health`, which returns the WebTransport URL template,
209-
certificate hash, and packet version needed by the client.
210-
The served browser UI receives the generated API access token automatically.
211-
212149
## JS/TS Tests
213150

214151
```ts
@@ -273,20 +210,15 @@ run `SimDeck: Open Simulator View` from the Command Palette. The extension
273210
opens the simulator inside a VS Code panel and auto-starts the local daemon
274211
when it is not already reachable.
275212

276-
## SimDeck Cloud
277-
278-
SimDeck Cloud uses the same server binary as its GitHub Actions provider. The
279-
provider workflow starts `simdeck serve` on the runner, exposes it through a
280-
tunnel, and lets the hosted control plane connect to the simulator with a
281-
one-time access token.
282-
283213
## Contributing
284214

285215
Contributors should read [CONTRIBUTING.md](CONTRIBUTING.md) for local build
286216
instructions, the dev workflow, and architecture notes.
287217

288-
## License
218+
## Copyright notice
219+
220+
Copyright [OpenJS Foundation](https://openjsf.org) and `NativeScript` contributors. All rights reserved. The [OpenJS Foundation](https://openjsf.org) has registered trademarks and uses trademarks. For a list of trademarks of the [OpenJS Foundation](https://openjsf.org), please see our [Trademark Policy](https://trademark-policy.openjsf.org/) and [Trademark List](https://trademark-list.openjsf.org/). Trademarks and logos not indicated on the [list of OpenJS Foundation trademarks](https://trademark-list.openjsf.org) are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.
289221

290-
Copyright 2026 Dj
222+
[The OpenJS Foundation](https://openjsf.org/) | [Terms of Use](https://terms-of-use.openjsf.org/) | [Privacy Policy](https://privacy-policy.openjsf.org/) | [OpenJS Foundation Bylaws](https://bylaws.openjsf.org/) | [Trademark Policy](https://trademark-policy.openjsf.org/) | [Trademark List](https://trademark-list.openjsf.org/) | [Cookie Policy](https://www.linuxfoundation.org/cookies/)
291223

292-
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).
224+
<h3 align="center">Made with ❤️</h3>

client/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
content="width=device-width, initial-scale=1.0, user-scalable=no"
88
/>
99
<meta name="color-scheme" content="light dark" />
10+
<link rel="favicon" type="image/png" href="/simdeck.png" />
1011
<title>SimDeck</title>
1112
<script>
1213
try {

client/public/simdeck.png

61.6 KB
Loading

client/src/app/AppShell.tsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const LOGICAL_INSPECTOR_MAX_DEPTH = 80;
9898
const AUTH_REQUIRED_MESSAGE = "SimDeck API access token is required.";
9999
const STREAM_TRANSPORT_STORAGE_KEY = "simdeck.streamTransport";
100100
const VIDEO_CODEC_STORAGE_KEY = "simdeck.videoCodec";
101+
const CODEC_SWITCH_SETTLE_MS = 180;
101102

102103
clearLegacyVolatileUiState();
103104

@@ -202,6 +203,10 @@ function readStoredVideoCodec(): VideoCodecMode {
202203
return isVideoCodecMode(stored) ? stored : "h264-software";
203204
}
204205

206+
function sleep(milliseconds: number): Promise<void> {
207+
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
208+
}
209+
205210
export function AppShell() {
206211
const [initialUiState] = useState(readPersistedUiState);
207212
const [initialSelectedUDID] = useState(
@@ -242,6 +247,7 @@ export function AppShell() {
242247
const [streamSettingsRevision, setStreamSettingsRevision] = useState(0);
243248
const [streamTransportMode, setStreamTransportMode] =
244249
useState<StreamTransportMode>(readStoredTransportMode);
250+
const [streamPaused, setStreamPaused] = useState(false);
245251
const [videoCodec, setVideoCodec] =
246252
useState<VideoCodecMode>(readStoredVideoCodec);
247253
const [viewMode, setViewMode] = useState<ViewMode>(
@@ -294,6 +300,7 @@ export function AppShell() {
294300
const zoomAnimationTimeoutRef = useRef<number>(0);
295301
const touchIndicatorTimeoutRef = useRef<number>(0);
296302
const gestureStartZoomRef = useRef(1);
303+
const codecSwitchInFlightRef = useRef(false);
297304
const accessibilityRequestIdRef = useRef(0);
298305
const accessibilityLoadingRef = useRef(false);
299306
const controlSocketRef = useRef<{
@@ -376,6 +383,7 @@ export function AppShell() {
376383
streamCanvasKey,
377384
} = useLiveStream({
378385
canvasElement: streamCanvasElement,
386+
paused: streamPaused,
379387
simulator: selectedSimulator,
380388
streamRevision: streamSettingsRevision,
381389
transportMode: streamTransportMode,
@@ -1213,17 +1221,38 @@ export function AppShell() {
12131221
}
12141222

12151223
function handleSelectVideoCodec(codec: VideoCodecMode) {
1216-
if (!selectedSimulator) {
1224+
if (!selectedSimulator || codecSwitchInFlightRef.current) {
12171225
return;
12181226
}
12191227
const udid = selectedSimulator.udid;
12201228
setVideoCodec(codec);
1221-
void runAction(async () => {
1222-
const response = await setSimulatorVideoCodec(udid, codec);
1223-
setVideoCodec(response.videoCodec);
1224-
setStreamSettingsRevision((current) => current + 1);
1225-
setStreamStamp(Date.now());
1226-
}, false);
1229+
void (async () => {
1230+
codecSwitchInFlightRef.current = true;
1231+
setStreamPaused(true);
1232+
closeControlSocket();
1233+
try {
1234+
await sleep(CODEC_SWITCH_SETTLE_MS);
1235+
const ok = await runAction(async () => {
1236+
const response = await setSimulatorVideoCodec(udid, codec);
1237+
setVideoCodec(response.videoCodec);
1238+
}, false);
1239+
await sleep(CODEC_SWITCH_SETTLE_MS);
1240+
setStreamSettingsRevision((current) => current + 1);
1241+
setStreamStamp(Date.now());
1242+
if (!ok) {
1243+
void fetchHealth()
1244+
.then((health) => {
1245+
if (isVideoCodecMode(health.videoCodec)) {
1246+
setVideoCodec(health.videoCodec);
1247+
}
1248+
})
1249+
.catch(() => {});
1250+
}
1251+
} finally {
1252+
codecSwitchInFlightRef.current = false;
1253+
setStreamPaused(false);
1254+
}
1255+
})();
12271256
}
12281257

12291258
async function submitPairing(event: FormEvent<HTMLFormElement>) {

client/src/features/stream/useLiveStream.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const DEBUG_RENDERER_SOFTWARE_PATTERN = /swiftshader|software|llvmpipe/i;
2727

2828
interface UseLiveStreamOptions {
2929
canvasElement: HTMLCanvasElement | null;
30+
paused?: boolean;
3031
simulator: SimulatorMetadata | null;
3132
streamRevision: number;
3233
transportMode: StreamTransportMode;
@@ -126,6 +127,7 @@ function buildClientTelemetryUrl(): string {
126127

127128
export function useLiveStream({
128129
canvasElement,
130+
paused = false,
129131
simulator,
130132
streamRevision,
131133
transportMode,
@@ -194,6 +196,7 @@ export function useLiveStream({
194196
useEffect(() => {
195197
if (
196198
!streamBackendResolved ||
199+
paused ||
197200
!canvasElement ||
198201
workerClientRef.current ||
199202
canvasElement.dataset.streamBackend !== transportBackend
@@ -282,7 +285,13 @@ export function useLiveStream({
282285
workerClient.destroy();
283286
workerClientRef.current = null;
284287
};
285-
}, [canvasElement, streamBackendResolved, transportBackend, transportMode]);
288+
}, [
289+
canvasElement,
290+
paused,
291+
streamBackendResolved,
292+
transportBackend,
293+
transportMode,
294+
]);
286295

287296
useEffect(() => {
288297
latestDecodedFramesRef.current = stats.decodedFrames;
@@ -400,7 +409,7 @@ export function useLiveStream({
400409
setError("");
401410
setFps(0);
402411

403-
if (!simulator?.isBooted) {
412+
if (paused || !simulator?.isBooted) {
404413
workerClient.disconnect();
405414
workerClient.clear();
406415
return;
@@ -414,6 +423,7 @@ export function useLiveStream({
414423
canvasElement,
415424
simulator?.isBooted,
416425
simulator?.udid,
426+
paused,
417427
streamBackendResolved,
418428
transportBackend,
419429
]);

0 commit comments

Comments
 (0)