Skip to content

Commit c345e5e

Browse files
committed
Improve Studio stream controls and rotation sync
1 parent 68a4426 commit c345e5e

25 files changed

Lines changed: 369 additions & 174 deletions

README.md

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ view inside the editor.
3636
## Features
3737

3838
- Local simulator video stream over browser-native WebRTC H.264
39-
- Full simulator control & inspection using private accessibility APIs
39+
- Full simulator control & inspection using private accessibility APIs - available using `simdeck` CLI
40+
- Real-time screen `describe` command using accessibility view tree - available in token-efficient format for agents
4041
- CoreSimulator chrome asset rendering for device bezels
4142
- NativeScript, React Native, UIKit and SwiftUI runtime inspector plugins to view app's view hierarchy live
4243
- `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
44+
- SimDeck Studio for sharing Simulator streams & automatic PR deployments to on-demand simulators
4445

4546
## Documentation
4647

@@ -59,33 +60,26 @@ To focus a specific simulator by name or UDID, pass it as the only argument:
5960
simdeck "iPhone 17 Pro Max"
6061
```
6162

62-
Use `simdeck ui --open` or `simdeck daemon start` when you want a reusable background daemon instead.
63-
The no-subcommand lifecycle shortcuts are `simdeck -d` for detached start, `simdeck -k` to kill the background daemon, and `simdeck -r` to restart it.
64-
The served loopback browser UI receives the generated API access token automatically. LAN browsers pair with the printed code before receiving the API cookie.
63+
`simdeck -d` for detached start, `simdeck -k` to kill the background daemon, and `simdeck -r` to restart it.
64+
65+
The served loopback browser UI receives the generated API access token automatically.
66+
LAN clients should pair with the printed code before receiving the API cookie.
6567

6668
SimDeck Studio providers run the daemon on loopback and use
6769
`scripts/studio-provider-bridge.mjs` for outbound control-plane communication
6870
with Studio. Studio hosts the browser UI and proxies SimDeck REST requests over
6971
that bridge while WebRTC media still negotiates directly between the browser and
7072
runner through ICE.
7173

72-
Expose a local simulator through Studio with one command:
74+
Expose a local simulator through SimDeck Studio with one command:
7375

7476
```sh
7577
simdeck studio expose "iPhone 17 Pro"
7678
```
7779

7880
The command starts or reuses the local daemon, creates an ephemeral Studio
7981
session, prints a unique `https://simdeck.djdev.me/simulator/...` URL, and keeps
80-
the outbound bridge alive until you press Ctrl-C. It uses software H.264 by
81-
default with realtime stream settings for remote viewing, and prints the active
82-
codec/profile when it starts. Studio defaults to the `smooth` stream quality
83-
profile (`1170` longest edge, dynamic up to `60` fps). Use
84-
`--stream-quality quality|balanced|fast|smooth|economy|ci-software` to override it,
85-
or pass `--video-codec hardware` when a dedicated hardware encoder is preferable.
86-
The remote viewer renders live video with the browser's native video element;
87-
the canvas is only used for input geometry. Remote viewers can choose 15, 30,
88-
or 60 fps in the browser stream menu.
82+
the outbound bridge alive until you press Ctrl-C.
8983

9084
CLI commands automatically use the same warm daemon:
9185

@@ -117,14 +111,6 @@ more important than full-resolution smoothness:
117111
simdeck daemon start --video-codec software --low-latency
118112
```
119113

120-
Local browser streams default to realtime WebRTC delivery with the `quality`
121-
profile on VideoToolbox H.264: full resolution, 120 fps, and a high bitrate floor. On
122-
high-refresh local displays, raise the local stream target explicitly:
123-
124-
```sh
125-
simdeck daemon restart --local-stream-fps 240
126-
```
127-
128114
Restart the CoreSimulator service layer when `simctl` reports a stale service
129115
version or the live display gets stuck before the first frame:
130116

@@ -255,7 +241,7 @@ React Fiber commits.
255241

256242
## VS Code
257243

258-
Install the `nativescript.simdeck` extension from the VS Code Marketplace, then
244+
Install the `nativescript.simdeck-vscode` extension from the VS Code Marketplace, then
259245
run `SimDeck: Open Simulator View` from the Command Palette. The extension
260246
opens the simulator inside a VS Code panel and auto-starts the local daemon
261247
when it is not already reachable.

cli/DFPrivateSimulatorDisplayBridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ NS_SWIFT_NAME(PrivateSimulatorDisplayBridge)
3838
@property (nonatomic, readonly, getter=isDisplayReady) BOOL displayReady;
3939
@property (nonatomic, readonly) NSString *displayStatus;
4040
@property (nonatomic, readonly) CGSize displaySize;
41+
@property (nonatomic, readonly) NSInteger rotationQuarterTurns;
4142

4243
- (nullable CVPixelBufferRef)copyPixelBuffer CF_RETURNS_RETAINED;
4344

cli/DFPrivateSimulatorDisplayBridge.m

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,32 @@ static double DFNormalizedDegrees(double value) {
14551455
return normalized;
14561456
}
14571457

1458+
static NSInteger DFRotationQuarterTurnsForDegrees(double degrees) {
1459+
NSInteger turns = (NSInteger)llround(DFNormalizedDegrees(degrees) / 90.0) % 4;
1460+
if (turns < 0) {
1461+
turns += 4;
1462+
}
1463+
return turns;
1464+
}
1465+
1466+
static void DFReconcileRotationWithDisplaySize(double *rotationDegrees, CGSize displaySize) {
1467+
if (rotationDegrees == NULL || displaySize.width <= 0.0 || displaySize.height <= 0.0) {
1468+
return;
1469+
}
1470+
1471+
double aspectDelta = fabs(displaySize.width - displaySize.height);
1472+
if (aspectDelta < 1.0) {
1473+
return;
1474+
}
1475+
1476+
NSInteger currentTurns = DFRotationQuarterTurnsForDegrees(*rotationDegrees);
1477+
BOOL displayIsLandscape = displaySize.width > displaySize.height;
1478+
BOOL rotationIsLandscape = (currentTurns % 2) != 0;
1479+
if (displayIsLandscape != rotationIsLandscape) {
1480+
*rotationDegrees = displayIsLandscape ? 90.0 : 0.0;
1481+
}
1482+
}
1483+
14581484
static NSArray<NSString *> * DFInterestingSelectorsForObject(id object) {
14591485
if (object == nil) {
14601486
return @[];
@@ -2758,6 +2784,7 @@ - (nullable instancetype)initWithUDID:(NSString *)udid
27582784
size_t width = CVPixelBufferGetWidth(pixelBuffer);
27592785
size_t height = CVPixelBufferGetHeight(pixelBuffer);
27602786
strongSelf->_displayPixelSize = CGSizeMake((CGFloat)width, (CGFloat)height);
2787+
DFReconcileRotationWithDisplaySize(&strongSelf->_deviceRotationDegrees, strongSelf->_displayPixelSize);
27612788
[strongSelf notifyDelegateOfFrame:pixelBuffer];
27622789
DFRunOnMainAsync(^{
27632790
if (strongSelf->_headlessHostWindow != nil) {
@@ -3435,6 +3462,21 @@ - (CGSize)displaySize {
34353462
return size;
34363463
}
34373464

3465+
- (NSInteger)rotationQuarterTurns {
3466+
__block NSInteger turns = 0;
3467+
dispatch_block_t work = ^{
3468+
turns = DFRotationQuarterTurnsForDegrees(self->_deviceRotationDegrees);
3469+
};
3470+
3471+
if (dispatch_get_specific(DFPrivateSimulatorCallbackQueueKey) != NULL) {
3472+
work();
3473+
} else {
3474+
dispatch_sync(_callbackQueue, work);
3475+
}
3476+
3477+
return turns;
3478+
}
3479+
34383480
- (BOOL)sendTouchAtNormalizedX:(double)normalizedX
34393481
normalizedY:(double)normalizedY
34403482
phase:(DFPrivateSimulatorTouchPhase)phase

cli/XCWPrivateSimulatorSession.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ typedef void (^XCWPrivateSimulatorEncodedFrameHandler)(NSData *sampleData,
2323
@property (nonatomic, readonly, getter=isDisplayReady) BOOL displayReady;
2424
@property (nonatomic, copy, readonly) NSString *displayStatus;
2525
@property (nonatomic, readonly) CGSize displaySize;
26+
@property (nonatomic, readonly) NSInteger rotationQuarterTurns;
2627
@property (nonatomic, readonly) NSUInteger frameSequence;
2728

2829
- (BOOL)waitUntilReadyWithTimeout:(NSTimeInterval)timeout;

cli/XCWPrivateSimulatorSession.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ - (CGSize)displaySize {
229229
return size;
230230
}
231231

232+
- (NSInteger)rotationQuarterTurns {
233+
return _displayBridge.rotationQuarterTurns;
234+
}
235+
232236
- (NSUInteger)frameSequence {
233237
__block NSUInteger sequence = 0;
234238
dispatch_sync(_stateQueue, ^{

cli/native/XCWNativeBridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ void xcw_native_session_request_refresh(void * _Nonnull handle);
7474
void xcw_native_session_request_keyframe(void * _Nonnull handle);
7575
void xcw_native_session_reconfigure_video_encoder(void * _Nonnull handle);
7676
char * _Nullable xcw_native_session_video_encoder_stats(void * _Nonnull handle, char * _Nullable * _Nullable error_message);
77+
int32_t xcw_native_session_rotation_quarter_turns(void * _Nonnull handle);
7778
bool xcw_native_session_send_touch(void * _Nonnull handle, double x, double y, const char * _Nonnull phase, char * _Nullable * _Nullable error_message);
7879
bool xcw_native_session_send_multitouch(void * _Nonnull handle, double x1, double y1, double x2, double y2, const char * _Nonnull phase, char * _Nullable * _Nullable error_message);
7980
bool xcw_native_session_send_key(void * _Nonnull handle, uint16_t key_code, uint32_t modifiers, char * _Nullable * _Nullable error_message);

cli/native/XCWNativeBridge.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,14 @@ void xcw_native_session_reconfigure_video_encoder(void *handle) {
702702
}
703703
}
704704

705+
int32_t xcw_native_session_rotation_quarter_turns(void *handle) {
706+
@autoreleasepool {
707+
NSInteger turns = [XCWNativeSessionFromHandle(handle) rotationQuarterTurns];
708+
NSInteger normalized = ((turns % 4) + 4) % 4;
709+
return (int32_t)normalized;
710+
}
711+
}
712+
705713
bool xcw_native_session_send_touch(void *handle, double x, double y, const char *phase, char **error_message) {
706714
@autoreleasepool {
707715
NSError *error = nil;

cli/native/XCWNativeSession.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
1616
- (void)requestKeyFrame;
1717
- (void)reconfigureVideoEncoder;
1818
- (NSDictionary *)videoEncoderStats;
19+
- (NSInteger)rotationQuarterTurns;
1920
- (BOOL)sendTouchAtX:(double)x
2021
y:(double)y
2122
phase:(NSString *)phase

cli/native/XCWNativeSession.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ - (NSDictionary *)videoEncoderStats {
106106
return [self.session videoEncoderStats];
107107
}
108108

109+
- (NSInteger)rotationQuarterTurns {
110+
return self.session.rotationQuarterTurns;
111+
}
112+
109113
- (BOOL)sendTouchAtX:(double)x
110114
y:(double)y
111115
phase:(NSString *)phase

client/src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface PrivateDisplayInfo {
44
displayWidth: number;
55
displayHeight: number;
66
frameSequence: number;
7+
rotationQuarterTurns?: number;
78
}
89

910
export interface SimulatorMetadata {

0 commit comments

Comments
 (0)