Skip to content

Commit 350ea44

Browse files
committed
Route remote realtime controls over WebRTC
1 parent 66b3639 commit 350ea44

9 files changed

Lines changed: 271 additions & 31 deletions

File tree

client/src/api/controls.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import type {
1111

1212
export type ControlMessage =
1313
| ({ type: "touch" } & TouchPayload)
14-
| ({ type: "key" } & KeyPayload);
14+
| ({ type: "key" } & KeyPayload)
15+
| { type: "dismissKeyboard" }
16+
| { type: "home" }
17+
| { type: "appSwitcher" }
18+
| { type: "rotateLeft" }
19+
| { type: "rotateRight" }
20+
| { type: "toggleAppearance" };
1521

1622
async function postSimulatorAction(
1723
udid: string,

client/src/app/AppShell.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -995,18 +995,19 @@ export function AppShell({
995995
return state;
996996
}, []);
997997

998-
function sendControl(udid: string, message: ControlMessage) {
998+
function sendControl(udid: string, message: ControlMessage): boolean {
999999
setLocalError("");
10001000
const encoded = JSON.stringify(message);
10011001
if (sendWebRtcControlMessage(encoded)) {
1002-
return;
1002+
return true;
10031003
}
10041004
const state = ensureControlSocket(udid);
10051005
if (state.socket.readyState === WebSocket.OPEN) {
10061006
state.socket.send(encoded);
10071007
} else {
10081008
state.pending.push(encoded);
10091009
}
1010+
return true;
10101011
}
10111012

10121013
useEffect(() => closeControlSocket, [closeControlSocket]);
@@ -1297,28 +1298,47 @@ export function AppShell({
12971298
if (!selectedSimulator) {
12981299
return;
12991300
}
1300-
void runAction(() => dismissKeyboard(selectedSimulator.udid), false);
1301+
if (
1302+
!sendControl(selectedSimulator.udid, { type: "dismissKeyboard" })
1303+
) {
1304+
void runAction(
1305+
() => dismissKeyboard(selectedSimulator.udid),
1306+
false,
1307+
);
1308+
}
13011309
}}
13021310
onHome={() => {
13031311
if (!selectedSimulator) {
13041312
return;
13051313
}
13061314
setAccessibilitySelectedId("");
13071315
setAccessibilityHoveredId(null);
1308-
void runAction(() => pressHome(selectedSimulator.udid), false);
1316+
if (!sendControl(selectedSimulator.udid, { type: "home" })) {
1317+
void runAction(() => pressHome(selectedSimulator.udid), false);
1318+
}
13091319
}}
13101320
onOpenAppSwitcher={() => {
13111321
if (!selectedSimulator) {
13121322
return;
13131323
}
13141324
setAccessibilitySelectedId("");
13151325
setAccessibilityHoveredId(null);
1316-
void runAction(() => openAppSwitcher(selectedSimulator.udid), false);
1326+
if (!sendControl(selectedSimulator.udid, { type: "appSwitcher" })) {
1327+
void runAction(
1328+
() => openAppSwitcher(selectedSimulator.udid),
1329+
false,
1330+
);
1331+
}
13171332
}}
13181333
onRotateLeft={() => {
13191334
if (!selectedSimulator) {
13201335
return;
13211336
}
1337+
if (sendControl(selectedSimulator.udid, { type: "rotateLeft" })) {
1338+
setRotationQuarterTurns((current) => (current + 3) % 4);
1339+
setStreamStamp(Date.now());
1340+
return;
1341+
}
13221342
void runAction(async () => {
13231343
await rotateLeft(selectedSimulator.udid);
13241344
setRotationQuarterTurns((current) => (current + 3) % 4);
@@ -1331,6 +1351,11 @@ export function AppShell({
13311351
if (!selectedSimulator) {
13321352
return;
13331353
}
1354+
if (sendControl(selectedSimulator.udid, { type: "rotateRight" })) {
1355+
setRotationQuarterTurns((current) => (current + 1) % 4);
1356+
setStreamStamp(Date.now());
1357+
return;
1358+
}
13341359
void runAction(async () => {
13351360
await rotateRight(selectedSimulator.udid);
13361361
setRotationQuarterTurns((current) => (current + 1) % 4);
@@ -1355,7 +1380,10 @@ export function AppShell({
13551380
if (!selectedSimulator) {
13561381
return;
13571382
}
1358-
void runAction(() => toggleAppearance(selectedSimulator.udid));
1383+
const encoded = JSON.stringify({ type: "toggleAppearance" });
1384+
if (!sendWebRtcControlMessage(encoded)) {
1385+
void runAction(() => toggleAppearance(selectedSimulator.udid));
1386+
}
13591387
}}
13601388
onToggleDebug={() => setDebugVisible((current) => !current)}
13611389
onToggleHierarchy={() => {

client/src/features/stream/streamWorkerClient.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export function sendWebRtcControlMessage(encoded: string): boolean {
2727
return true;
2828
}
2929

30+
export function sendWebRtcClientStats(stats: unknown): boolean {
31+
if (activeWebRtcControlChannel?.readyState !== "open") {
32+
return false;
33+
}
34+
activeWebRtcControlChannel.send(
35+
JSON.stringify({ stats, type: "clientStats" }),
36+
);
37+
return true;
38+
}
39+
3040
export function buildStreamTarget(
3141
udid: string,
3242
options: { remote?: boolean } = {},
@@ -532,6 +542,9 @@ class WebRtcStreamClient implements StreamClientBackend {
532542
url: window.location.href,
533543
userAgent: window.navigator.userAgent,
534544
};
545+
if (sendWebRtcClientStats(payload) || this.remoteMode) {
546+
return;
547+
}
535548
void fetch(
536549
new URL(apiUrl("/api/client-stream-stats"), window.location.href),
537550
{

client/src/features/stream/useLiveStream.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createEmptyStreamStats } from "./stats";
88
import {
99
buildStreamTarget,
1010
canUseWebRtc,
11+
sendWebRtcClientStats,
1112
StreamWorkerClient,
1213
type StreamBackend,
1314
} from "./streamWorkerClient";
@@ -254,21 +255,25 @@ export function useLiveStream({
254255
const postTelemetry = () => {
255256
const latestStats = latestStatsRef.current;
256257
const latestStatus = latestStatusRef.current;
258+
const payload = {
259+
...latestStats,
260+
appFps: latestFpsRef.current,
261+
clientId: clientTelemetryIdRef.current,
262+
focused: document.hasFocus(),
263+
kind: "page",
264+
pageFps: pageFpsRef.current,
265+
status: latestStatus.state,
266+
timestampMs: Date.now(),
267+
udid: simulator.udid,
268+
url: window.location.href,
269+
userAgent: window.navigator.userAgent,
270+
visibilityState: document.visibilityState,
271+
};
272+
if (sendWebRtcClientStats(payload) || remote) {
273+
return;
274+
}
257275
void fetch(buildClientTelemetryUrl(), {
258-
body: JSON.stringify({
259-
...latestStats,
260-
appFps: latestFpsRef.current,
261-
clientId: clientTelemetryIdRef.current,
262-
focused: document.hasFocus(),
263-
kind: "page",
264-
pageFps: pageFpsRef.current,
265-
status: latestStatus.state,
266-
timestampMs: Date.now(),
267-
udid: simulator.udid,
268-
url: window.location.href,
269-
userAgent: window.navigator.userAgent,
270-
visibilityState: document.visibilityState,
271-
}),
276+
body: JSON.stringify(payload),
272277
cache: "no-store",
273278
headers: apiHeaders(),
274279
method: "POST",

server/src/api/routes.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ pub(crate) enum ControlMessage {
191191
key_code: u16,
192192
modifiers: Option<u32>,
193193
},
194+
DismissKeyboard,
195+
Home,
196+
AppSwitcher,
197+
RotateLeft,
198+
RotateRight,
199+
ToggleAppearance,
194200
}
195201

196202
#[derive(Deserialize)]
@@ -1279,6 +1285,14 @@ pub(crate) async fn run_control_message(
12791285
key_code,
12801286
modifiers,
12811287
} => session.send_key(key_code, modifiers.unwrap_or(0)),
1288+
ControlMessage::DismissKeyboard => session.send_key(41, 0),
1289+
ControlMessage::Home => session.press_home(),
1290+
ControlMessage::AppSwitcher => session.open_app_switcher(),
1291+
ControlMessage::RotateLeft => session.rotate_left(),
1292+
ControlMessage::RotateRight => session.rotate_right(),
1293+
ControlMessage::ToggleAppearance => Err(AppError::bad_request(
1294+
"`toggleAppearance` requires the WebRTC control channel.",
1295+
)),
12821296
})
12831297
.await
12841298
.map_err(|error| AppError::internal(format!("Failed to join control task: {error}")))?

server/src/native/bridge.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,46 @@ impl NativeSession {
686686
)
687687
}
688688
}
689+
690+
pub fn press_home(&self) -> Result<(), AppError> {
691+
unsafe {
692+
let mut error = ptr::null_mut();
693+
bool_result(
694+
ffi::xcw_native_session_press_home(self.handle, &mut error),
695+
error,
696+
)
697+
}
698+
}
699+
700+
pub fn open_app_switcher(&self) -> Result<(), AppError> {
701+
unsafe {
702+
let mut error = ptr::null_mut();
703+
bool_result(
704+
ffi::xcw_native_session_open_app_switcher(self.handle, &mut error),
705+
error,
706+
)
707+
}
708+
}
709+
710+
pub fn rotate_left(&self) -> Result<(), AppError> {
711+
unsafe {
712+
let mut error = ptr::null_mut();
713+
bool_result(
714+
ffi::xcw_native_session_rotate_left(self.handle, &mut error),
715+
error,
716+
)
717+
}
718+
}
719+
720+
pub fn rotate_right(&self) -> Result<(), AppError> {
721+
unsafe {
722+
let mut error = ptr::null_mut();
723+
bool_result(
724+
ffi::xcw_native_session_rotate_right(self.handle, &mut error),
725+
error,
726+
)
727+
}
728+
}
689729
}
690730

691731
impl Drop for NativeSession {

server/src/native/ffi.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,22 @@ unsafe extern "C" {
202202
modifiers: u32,
203203
error_message: *mut *mut c_char,
204204
) -> bool;
205+
pub fn xcw_native_session_press_home(
206+
handle: *mut c_void,
207+
error_message: *mut *mut c_char,
208+
) -> bool;
209+
pub fn xcw_native_session_open_app_switcher(
210+
handle: *mut c_void,
211+
error_message: *mut *mut c_char,
212+
) -> bool;
213+
pub fn xcw_native_session_rotate_right(
214+
handle: *mut c_void,
215+
error_message: *mut *mut c_char,
216+
) -> bool;
217+
pub fn xcw_native_session_rotate_left(
218+
handle: *mut c_void,
219+
error_message: *mut *mut c_char,
220+
) -> bool;
205221

206222
pub fn xcw_native_free_string(value: *mut c_char);
207223
pub fn xcw_native_free_bytes(bytes: xcw_native_owned_bytes);

server/src/simulators/session.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,22 @@ impl SimulatorSession {
182182
self.inner.native.send_key(key_code, modifiers)
183183
}
184184

185+
pub fn press_home(&self) -> Result<(), AppError> {
186+
self.inner.native.press_home()
187+
}
188+
189+
pub fn open_app_switcher(&self) -> Result<(), AppError> {
190+
self.inner.native.open_app_switcher()
191+
}
192+
193+
pub fn rotate_left(&self) -> Result<(), AppError> {
194+
self.inner.native.rotate_left()
195+
}
196+
197+
pub fn rotate_right(&self) -> Result<(), AppError> {
198+
self.inner.native.rotate_right()
199+
}
200+
185201
pub fn snapshot(&self) -> serde_json::Value {
186202
serde_json::json!({
187203
"displayReady": self.inner.display_ready.load(Ordering::Relaxed),

0 commit comments

Comments
 (0)