| 1 | import type { DeviceHubAccess } from "@t3tools/client-runtime/device/hub-access"; |
| 2 | import type { DevicePlatform } from "@t3tools/contracts"; |
| 3 | |
| 4 | export 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 | |
| 18 | export 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 | |
| 26 | export 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 | } |