Skip to content
Source
Sign inSign up
Open History to see the last commit

preview-stream-document.ts

80 lines3.7 KB

1import type { DeviceHubAccess } from "@t3tools/client-runtime/device/hub-access";
2import type { PreviewStreamHostSetup } from "@t3tools/contracts";
3import type {
4 PreviewStreamControl,
5 PreviewStreamDownload,
6 PreviewStreamFileChooser,
7} from "@t3tools/client-runtime/preview/server-browser-stream";
8​
9export interface PreviewStreamConfiguration {
10 readonly access: DeviceHubAccess;
11 readonly threadId: string;
12 readonly tabId: string;
13 readonly interactive: boolean;
14 readonly background: string;
16​
17export type PreviewStreamMessage =
18 | ({ readonly type: "control" } & PreviewStreamControl)
19 | {
20 readonly type: "status";
21 readonly status: "connecting" | "streaming" | "error";
22 readonly detail?: string;
23 }
24 | { readonly type: "unauthorized" }
25 | { readonly type: "gone" }
26 | ({ readonly type: "hostSetup" } & PreviewStreamHostSetup)
27 | { readonly type: "viewport"; readonly width: number; readonly height: number }
28 | { readonly type: "clipboard"; readonly text: string }
29 | ({ readonly type: "download" } & PreviewStreamDownload)
30 | { readonly type: "fileChooser"; readonly chooser: PreviewStreamFileChooser | null }
31 | {
32 readonly type: "pictureInPicture";
33 readonly supported: boolean;
34 readonly active: boolean;
35 readonly detail?: string;
36 };
37​
38export function previewStreamDocument(configuration: string, script: string) {
39 const safeConfiguration = configuration.replace(/</g, "\\u003c");
40 const safeScript = script.replace(/<\/script/gi, "<\\/script");
41 const failure = `window.ReactNativeWebView.postMessage(JSON.stringify({type:"status",status:"error",detail:"Browser viewer stopped unexpectedly."}));`;
42 return `<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
43<style>
44 html, body { height: 100%; overflow: hidden; }
45 body { margin: 0; }
46 body > div { position: fixed; left: 0; top: 0; width: 100%; height: 100%; overflow: hidden; }
47 canvas, video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
48 canvas { touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
49 video { pointer-events: none; }
50 /* The agent's pointer, positioned over the letterboxed frame by the viewer script. */
51 .agent-cursor {
52 position: absolute; left: 0; top: 0; width: 20px; height: 20px; pointer-events: none;
53 opacity: 0; transition: opacity 150ms ease-out, transform 150ms ease-out;
54 }
55 .agent-cursor svg { position: relative; display: block; filter: drop-shadow(0 1px 1px rgba(0,0,0,.35)); }
56 .agent-cursor .ping {
57 position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; border-radius: 50%;
58 background: rgba(59,130,246,.3); animation: agent-cursor-ping 600ms ease-out forwards;
59 }
60 @keyframes agent-cursor-ping { from { transform: scale(.6); opacity: 1; } to { transform: scale(2.2); opacity: 0; } }
61 @media (prefers-reduced-motion: reduce) {
62 .agent-cursor { transition: none; }
63 .agent-cursor .ping { animation: none; opacity: 0; }
64 }
65 /* Pinned so focus never scrolls; 16px keeps iOS from zooming on focus. */
66 textarea {
67 position: fixed; left: 0; top: 0; width: 1px; height: 1px;
68 padding: 0; margin: -1px; border: 0; overflow: hidden;
69 clip: rect(0, 0, 0, 0); white-space: nowrap; font-size: 16px;
70 }
71</style></head><body><script>window.addEventListener("error",function(){${failure}});window.addEventListener("unhandledrejection",function(){${failure}});\n${safeScript}\ntry{T3PreviewStream.start(${safeConfiguration});}catch{${failure}}</script></body></html>`;
73​
74export function previewStreamMessage(data: string): PreviewStreamMessage | null {
75 try {
76 return JSON.parse(data) as PreviewStreamMessage;
77 } catch {
78 return null;
79 }