1import type { DeviceHubAccess } from "@t3tools/client-runtime/device/hub-access";
2import type { DevicePlatform } from "@t3tools/contracts";
3
4export interface DeviceStreamConfiguration {
5 readonly access: DeviceHubAccess;
6 readonly platform: DevicePlatform;
7 readonly deviceId: string;
8 readonly colors: {
9 readonly background: string;
10 readonly foreground: string;
11 readonly muted: string;
12 readonly buttonBackground: string;
13 readonly buttonForeground: string;
14 readonly buttonBorder: string;
15 };
16}
17
18export function deviceStreamDocument(configuration: string, script: string) {
19 // Tickets and device names are data, including any HTML delimiter characters.
20 const safeConfiguration = configuration.replace(/</g, "\\u003c");
21 const safeScript = script.replace(/<\/script/gi, "<\\/script");
22 const failure = `window.ReactNativeWebView.postMessage(JSON.stringify({type:"status",status:"error",detail:"Device viewer stopped unexpectedly."}));`;
23 return `<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"></head><body><script>window.addEventListener("error",function(){${failure}});window.addEventListener("unhandledrejection",function(){${failure}});\n${safeScript}\ntry{T3DeviceStream.start(${safeConfiguration});}catch{${failure}}</script></body></html>`;
24}
25
26export function deviceStreamMessage(data: string) {
27 try {
28 const message: unknown = JSON.parse(data);
29 if (typeof message !== "object" || message === null || !("type" in message)) return null;
30 if (message.type === "unauthorized" || message.type === "retry") {
31 return { type: message.type } as const;
32 }
33 if (
34 message.type === "input" &&
35 "connected" in message &&
36 typeof message.connected === "boolean"
37 ) {
38 return { type: message.type, connected: message.connected } as const;
39 }
40 if (
41 message.type === "status" &&
42 "status" in message &&
43 (message.status === "connecting" ||
44 message.status === "streaming" ||
45 message.status === "error") &&
46 (!("detail" in message) || typeof message.detail === "string")
47 ) {
48 return {
49 type: message.type,
50 status: message.status,
51 detail:
52 "detail" in message && typeof message.detail === "string" ? message.detail : undefined,
53 } as const;
54 }
55 } catch {
56 // Ignore messages that are not part of the stream bridge.
57 }
58 return null;
59}