@alivelabs/expo-orchestrator-react-client 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -3
- package/dist/components/SessionViewer.d.ts.map +1 -1
- package/dist/components/SessionViewer.js +2 -2
- package/dist/components/SimulatorScreen.d.ts +8 -3
- package/dist/components/SimulatorScreen.d.ts.map +1 -1
- package/dist/components/SimulatorScreen.js +27 -18
- package/dist/decoder.d.ts +34 -0
- package/dist/decoder.d.ts.map +1 -0
- package/dist/decoder.js +187 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +15 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/useExpoCiSession.d.ts.map +1 -1
- package/dist/useExpoCiSession.js +34 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,39 +28,48 @@ function App() {
|
|
|
28
28
|
|
|
29
29
|
## Hook usage
|
|
30
30
|
|
|
31
|
+
The live screen is an **H.264 stream decoded with WebCodecs** onto a `<canvas>`.
|
|
32
|
+
The hook owns the decoder — pass `attachCanvas` to a `<canvas ref>`:
|
|
33
|
+
|
|
31
34
|
```tsx
|
|
32
35
|
import { useExpoCiSession } from "@alivelabs/expo-orchestrator-react-client";
|
|
33
36
|
|
|
34
37
|
function BuildPage() {
|
|
35
|
-
const { status, logs,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const { status, logs, attachCanvas, videoSupported, connected, sendInput, reconnect } =
|
|
39
|
+
useExpoCiSession({
|
|
40
|
+
sessionId: "abc-123",
|
|
41
|
+
apiToken: "your-api-token",
|
|
42
|
+
baseUrl: "https://your-orchestrator.example.com",
|
|
43
|
+
autoConnect: true, // default
|
|
44
|
+
maxLogs: 2000, // default
|
|
45
|
+
});
|
|
42
46
|
|
|
43
47
|
return (
|
|
44
48
|
<div>
|
|
45
49
|
<p>Status: {status} — {connected ? "live" : "disconnected"}</p>
|
|
46
|
-
{
|
|
47
|
-
<button onClick={() => sendInput({ type: "
|
|
50
|
+
{videoSupported ? <canvas ref={attachCanvas} /> : <p>Video not supported in this browser.</p>}
|
|
51
|
+
<button onClick={() => sendInput({ type: "button", button: "home" })}>Home</button>
|
|
48
52
|
{!connected && <button onClick={reconnect}>Reconnect</button>}
|
|
49
53
|
</div>
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
```
|
|
53
57
|
|
|
58
|
+
WebCodecs needs a modern browser (Chrome/Edge, Safari 16.4+, recent Firefox) and
|
|
59
|
+
a secure context (HTTPS or localhost); `videoSupported` is `false` otherwise. For
|
|
60
|
+
full control you can import `SimulatorDecoder` and drive your own canvas.
|
|
61
|
+
|
|
54
62
|
## Exports reference
|
|
55
63
|
|
|
56
64
|
| Export | Kind | Description |
|
|
57
65
|
|---|---|---|
|
|
58
66
|
| `SessionViewer` | Component | All-in-one: status badge, simulator screen, log console, interaction controls |
|
|
59
|
-
| `SimulatorScreen` | Component | Renders the
|
|
67
|
+
| `SimulatorScreen` | Component | Renders the live H.264 stream to a canvas; click = tap, click-drag = swipe |
|
|
60
68
|
| `LogConsole` | Component | Scrollable, color-coded, auto-scrolling log viewer |
|
|
61
69
|
| `StatusBadge` | Component | Colored pill showing session status |
|
|
62
|
-
| `useExpoCiSession` | Hook | React hook wrapping `ExpoCiClient`
|
|
70
|
+
| `useExpoCiSession` | Hook | React hook wrapping `ExpoCiClient` + the WebCodecs decoder |
|
|
63
71
|
| `ExpoCiClient` | Class | Framework-agnostic client: REST + WebSocket with auto-reconnect |
|
|
72
|
+
| `SimulatorDecoder` | Class | WebCodecs H.264 decoder painting to a `<canvas>` (advanced) |
|
|
64
73
|
|
|
65
74
|
### `<SessionViewer>` props
|
|
66
75
|
|
|
@@ -75,9 +84,13 @@ function BuildPage() {
|
|
|
75
84
|
|
|
76
85
|
| Prop | Type | Required | Description |
|
|
77
86
|
|---|---|---|---|
|
|
78
|
-
| `
|
|
87
|
+
| `canvasRef` | `(canvas: HTMLCanvasElement \| null) => void` | yes | Ref callback wiring the canvas to the decoder (pass `attachCanvas`) |
|
|
88
|
+
| `videoSupported` | `boolean` | yes | Whether WebCodecs is available; shows a notice when `false` |
|
|
89
|
+
| `hasFrame` | `boolean` | yes | Whether a frame has painted yet (drives the placeholder) |
|
|
79
90
|
| `onTap` | `(input: { type: "tap"; x: number; y: number }) => void` | no | Called on click |
|
|
80
|
-
| `onSwipe` | `(input: { type: "swipe";
|
|
91
|
+
| `onSwipe` | `(input: { type: "swipe"; startX; startY; endX; endY }) => void` | no | Called on drag |
|
|
92
|
+
| `onKeyInput` | `(input: { type: "type" \| "key"; … }) => void` | no | Called for keystrokes while focused |
|
|
93
|
+
| `maxHeight` | `number \| string` | no | Cap on rendered height (default `80vh`) |
|
|
81
94
|
|
|
82
95
|
### `<LogConsole>` props
|
|
83
96
|
|
|
@@ -115,9 +128,10 @@ client.getScreenshotObjectUrl() // GET /api/sessions/:id/screenshot → object
|
|
|
115
128
|
client.connect() // Open WebSocket (auto-reconnect with exponential backoff)
|
|
116
129
|
client.disconnect() // Close WebSocket and remove all listeners
|
|
117
130
|
|
|
118
|
-
// Typed event emitter
|
|
119
|
-
|
|
131
|
+
// Typed event emitter. Video arrives as raw encoded chunks (ArrayBuffer) —
|
|
132
|
+
// feed them to a SimulatorDecoder, or let the hook do it for you.
|
|
133
|
+
const off = client.on("video-chunk", (chunk) => decoder.push(chunk));
|
|
120
134
|
off(); // unsubscribe
|
|
121
135
|
```
|
|
122
136
|
|
|
123
|
-
Events: `open`, `close`, `connected`, `log`, `video-
|
|
137
|
+
Events: `open`, `close`, `connected`, `log`, `video-chunk`, `status`, `error`.
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AAIpB,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAGxE,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEpC,cAAM,iBAAiB,CAAC,MAAM,SAAS,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwD;IAElF,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IASxF,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInF,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAClC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GACtD,IAAI;IAQP,kBAAkB,IAAI,IAAI;CAG3B;AAUD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAa,SAAQ,iBAAiB,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,OAAiC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB;YAU7E,SAAS;IAgBvB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAIpC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU3D,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAc/C,OAAO,KAAK,KAAK,GAKhB;IAED,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,UAAU;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AAIpB,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAGxE,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEpC,cAAM,iBAAiB,CAAC,MAAM,SAAS,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwD;IAElF,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IASxF,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInF,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAClC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GACtD,IAAI;IAQP,kBAAkB,IAAI,IAAI;CAG3B;AAUD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAa,SAAQ,iBAAiB,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,OAAiC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB;YAU7E,SAAS;IAgBvB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAIpC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU3D,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAc/C,OAAO,KAAK,KAAK,GAKhB;IAED,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,UAAU;IA6DlB,OAAO,CAAC,iBAAiB;IASzB,UAAU,IAAI,IAAI;CAYnB"}
|
package/dist/client.js
CHANGED
|
@@ -97,6 +97,8 @@ export class ExpoCiClient extends TypedEventEmitter {
|
|
|
97
97
|
if (this.destroyed)
|
|
98
98
|
return;
|
|
99
99
|
const socket = new WebSocket(this.wsUrl);
|
|
100
|
+
// Video arrives as binary frames; logs/status as JSON text.
|
|
101
|
+
socket.binaryType = "arraybuffer";
|
|
100
102
|
this.ws = socket;
|
|
101
103
|
socket.addEventListener("open", () => {
|
|
102
104
|
if (this.destroyed) {
|
|
@@ -109,6 +111,11 @@ export class ExpoCiClient extends TypedEventEmitter {
|
|
|
109
111
|
socket.addEventListener("message", (event) => {
|
|
110
112
|
if (this.destroyed)
|
|
111
113
|
return;
|
|
114
|
+
// Binary = an encoded H.264 chunk; hand it straight to the decoder.
|
|
115
|
+
if (event.data instanceof ArrayBuffer) {
|
|
116
|
+
this.emit("video-chunk", event.data);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
112
119
|
let msg;
|
|
113
120
|
try {
|
|
114
121
|
msg = JSON.parse(event.data);
|
|
@@ -123,9 +130,6 @@ export class ExpoCiClient extends TypedEventEmitter {
|
|
|
123
130
|
case "log":
|
|
124
131
|
this.emit("log", msg);
|
|
125
132
|
break;
|
|
126
|
-
case "video-frame":
|
|
127
|
-
this.emit("video-frame", msg);
|
|
128
|
-
break;
|
|
129
133
|
case "status":
|
|
130
134
|
this.emit("status", msg);
|
|
131
135
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionViewer.d.ts","sourceRoot":"","sources":["../../src/components/SessionViewer.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAyHD,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"SessionViewer.d.ts","sourceRoot":"","sources":["../../src/components/SessionViewer.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAyHD,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,kBAAkB,2CAwK5F"}
|
|
@@ -111,7 +111,7 @@ const PRESETS = [
|
|
|
111
111
|
{ label: "Enter", input: { type: "key", code: "Enter" } },
|
|
112
112
|
];
|
|
113
113
|
export function SessionViewer({ sessionId, apiToken, baseUrl, className }) {
|
|
114
|
-
const { status, logs,
|
|
114
|
+
const { status, logs, attachCanvas, videoSupported, frameSize, connected, error, sendInput, reconnect, } = useExpoCiSession({
|
|
115
115
|
baseUrl,
|
|
116
116
|
sessionId,
|
|
117
117
|
apiToken,
|
|
@@ -160,7 +160,7 @@ export function SessionViewer({ sessionId, apiToken, baseUrl, className }) {
|
|
|
160
160
|
fontSize: "11px",
|
|
161
161
|
color: connected ? "#4ade80" : "#6b7280",
|
|
162
162
|
fontWeight: 500,
|
|
163
|
-
}, children: connected ? "● live" : "○ disconnected" }), !connected && (_jsx("button", { type: "button", style: btnStyle("secondary"), onClick: reconnect, children: "Reconnect" }))] })] }), error && _jsxs("div", { style: errorBannerStyle, children: ["\u26A0 ", error] }), _jsxs("div", { style: bodyStyle, children: [_jsx("div", { style: simulatorColStyle, children: _jsx(SimulatorScreen, {
|
|
163
|
+
}, children: connected ? "● live" : "○ disconnected" }), !connected && (_jsx("button", { type: "button", style: btnStyle("secondary"), onClick: reconnect, children: "Reconnect" }))] })] }), error && _jsxs("div", { style: errorBannerStyle, children: ["\u26A0 ", error] }), _jsxs("div", { style: bodyStyle, children: [_jsx("div", { style: simulatorColStyle, children: _jsx(SimulatorScreen, { canvasRef: attachCanvas, videoSupported: videoSupported, hasFrame: frameSize !== null, onTap: handleTap, onSwipe: handleSwipe, onKeyInput: handleKeyInput }) }), _jsxs("div", { style: rightColStyle, children: [_jsxs("div", { style: controlsStyle, children: [_jsx("div", { style: {
|
|
164
164
|
fontSize: "11px",
|
|
165
165
|
fontWeight: 600,
|
|
166
166
|
color: "#8b949e",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { SimulatorInput } from "../types.js";
|
|
2
2
|
interface SimulatorScreenProps {
|
|
3
|
-
|
|
3
|
+
/** Ref callback wiring the rendering `<canvas>` to the decoder (from the hook). */
|
|
4
|
+
canvasRef: (canvas: HTMLCanvasElement | null) => void;
|
|
5
|
+
/** Whether this browser can decode the live H.264 stream (WebCodecs present). */
|
|
6
|
+
videoSupported: boolean;
|
|
7
|
+
/** True once the first frame has painted — drives the placeholder/canvas swap. */
|
|
8
|
+
hasFrame: boolean;
|
|
4
9
|
onTap?: (input: Extract<SimulatorInput, {
|
|
5
10
|
type: "tap";
|
|
6
11
|
}>) => void;
|
|
@@ -13,11 +18,11 @@ interface SimulatorScreenProps {
|
|
|
13
18
|
} | {
|
|
14
19
|
type: "key";
|
|
15
20
|
}>) => void;
|
|
16
|
-
/** Cap on the rendered simulator height; the
|
|
21
|
+
/** Cap on the rendered simulator height; the canvas scales down proportionally
|
|
17
22
|
* to fit. Accepts any CSS length. Defaults to `80vh` so the device never
|
|
18
23
|
* exceeds the viewport on a tall phone like iPhone 17 Pro Max. */
|
|
19
24
|
maxHeight?: number | string;
|
|
20
25
|
}
|
|
21
|
-
export declare function SimulatorScreen({
|
|
26
|
+
export declare function SimulatorScreen({ canvasRef, videoSupported, hasFrame, onTap, onSwipe, onKeyInput, maxHeight, }: SimulatorScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
22
27
|
export {};
|
|
23
28
|
//# sourceMappingURL=SimulatorScreen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SimulatorScreen.d.ts","sourceRoot":"","sources":["../../src/components/SimulatorScreen.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAA0C,MAAM,aAAa,CAAC;AAE1F,UAAU,oBAAoB;IAC5B,
|
|
1
|
+
{"version":3,"file":"SimulatorScreen.d.ts","sourceRoot":"","sources":["../../src/components/SimulatorScreen.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAA0C,MAAM,aAAa,CAAC;AAE1F,UAAU,oBAAoB;IAC5B,mFAAmF;IACnF,SAAS,EAAE,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;IACtD,iFAAiF;IACjF,cAAc,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,EAAE;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;IACtE,oFAAoF;IACpF,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;IAC1F;;uEAEmE;IACnE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAmJD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,cAAc,EACd,QAAQ,EACR,KAAK,EACL,OAAO,EACP,UAAU,EACV,SAAkB,GACnB,EAAE,oBAAoB,2CA2JtB"}
|
|
@@ -112,35 +112,44 @@ const basePlaceholderStyle = {
|
|
|
112
112
|
// here or the stacked glyph + text collapse onto each other.
|
|
113
113
|
lineHeight: 1.4,
|
|
114
114
|
};
|
|
115
|
-
const
|
|
116
|
-
display: "block",
|
|
115
|
+
const baseCanvasStyle = {
|
|
117
116
|
maxWidth: "100%",
|
|
118
117
|
width: "auto",
|
|
119
118
|
height: "auto",
|
|
120
119
|
pointerEvents: "none",
|
|
121
120
|
};
|
|
122
|
-
/** Convert a rendered
|
|
121
|
+
/** Convert a rendered pixel position to the canvas's natural (device) pixel coords. */
|
|
123
122
|
function toNatural(el, clientX, clientY) {
|
|
124
123
|
const rect = el.getBoundingClientRect();
|
|
125
|
-
const scaleX = el.
|
|
126
|
-
const scaleY = el.
|
|
124
|
+
const scaleX = el.width / rect.width;
|
|
125
|
+
const scaleY = el.height / rect.height;
|
|
127
126
|
return {
|
|
128
127
|
x: Math.round((clientX - rect.left) * scaleX),
|
|
129
128
|
y: Math.round((clientY - rect.top) * scaleY),
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
|
-
export function SimulatorScreen({
|
|
133
|
-
const
|
|
131
|
+
export function SimulatorScreen({ canvasRef, videoSupported, hasFrame, onTap, onSwipe, onKeyInput, maxHeight = "80vh", }) {
|
|
132
|
+
const canvasElRef = useRef(null);
|
|
134
133
|
const dragRef = useRef(null);
|
|
135
134
|
const [focused, setFocused] = useState(false);
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
135
|
+
// The canvas only drives layout once a frame has painted (it's 0×0 before);
|
|
136
|
+
// until then the placeholder gives the device its iPhone-aspect footprint.
|
|
137
|
+
const canvasStyle = {
|
|
138
|
+
...baseCanvasStyle,
|
|
139
|
+
maxHeight,
|
|
140
|
+
display: hasFrame ? "block" : "none",
|
|
141
|
+
};
|
|
139
142
|
const placeholderStyle = { ...basePlaceholderStyle, height: maxHeight };
|
|
143
|
+
// Wire the canvas into both the decoder (parent callback) and our local ref
|
|
144
|
+
// for pointer-coordinate math.
|
|
145
|
+
const setCanvas = useCallback((el) => {
|
|
146
|
+
canvasElRef.current = el;
|
|
147
|
+
canvasRef(el);
|
|
148
|
+
}, [canvasRef]);
|
|
140
149
|
const handleMouseDown = useCallback((e) => {
|
|
141
|
-
if (!
|
|
150
|
+
if (!canvasElRef.current)
|
|
142
151
|
return;
|
|
143
|
-
const natural = toNatural(
|
|
152
|
+
const natural = toNatural(canvasElRef.current, e.clientX, e.clientY);
|
|
144
153
|
dragRef.current = {
|
|
145
154
|
startX: e.clientX,
|
|
146
155
|
startY: e.clientY,
|
|
@@ -151,7 +160,7 @@ export function SimulatorScreen({ frame, onTap, onSwipe, onKeyInput, maxHeight =
|
|
|
151
160
|
const [ripple, setRipple] = useState(null);
|
|
152
161
|
const rippleCounter = useRef(0);
|
|
153
162
|
const handleMouseUp = useCallback((e) => {
|
|
154
|
-
if (!dragRef.current || !
|
|
163
|
+
if (!dragRef.current || !canvasElRef.current)
|
|
155
164
|
return;
|
|
156
165
|
const drag = dragRef.current;
|
|
157
166
|
dragRef.current = null;
|
|
@@ -160,17 +169,17 @@ export function SimulatorScreen({ frame, onTap, onSwipe, onKeyInput, maxHeight =
|
|
|
160
169
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
161
170
|
if (dist < SWIPE_THRESHOLD_PX) {
|
|
162
171
|
// Tap
|
|
163
|
-
const natural = toNatural(
|
|
172
|
+
const natural = toNatural(canvasElRef.current, e.clientX, e.clientY);
|
|
164
173
|
onTap?.({ type: "tap", x: natural.x, y: natural.y });
|
|
165
174
|
// Show ripple at rendered coords
|
|
166
|
-
const rect =
|
|
175
|
+
const rect = canvasElRef.current.getBoundingClientRect();
|
|
167
176
|
const id = ++rippleCounter.current;
|
|
168
177
|
setRipple({ x: e.clientX - rect.left, y: e.clientY - rect.top, id });
|
|
169
178
|
setTimeout(() => setRipple((r) => (r?.id === id ? null : r)), 500);
|
|
170
179
|
}
|
|
171
180
|
else {
|
|
172
181
|
// Swipe
|
|
173
|
-
const naturalEnd = toNatural(
|
|
182
|
+
const naturalEnd = toNatural(canvasElRef.current, e.clientX, e.clientY);
|
|
174
183
|
onSwipe?.({
|
|
175
184
|
type: "swipe",
|
|
176
185
|
startX: drag.naturalStartX,
|
|
@@ -192,13 +201,13 @@ export function SimulatorScreen({ frame, onTap, onSwipe, onKeyInput, maxHeight =
|
|
|
192
201
|
onKeyInput(input);
|
|
193
202
|
}, [onKeyInput]);
|
|
194
203
|
const frameStyle = focused ? { ...deviceFrameStyle, ...focusedFrameStyle } : deviceFrameStyle;
|
|
195
|
-
return (_jsx("div", { style: frameStyle, children: _jsxs("div", { style: wrapperStyle, role: "application", "aria-label": "iOS simulator \u2014 click to focus; tap, drag to swipe, type to send keys", tabIndex: 0, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onKeyDown: handleKeyDown, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: [
|
|
204
|
+
return (_jsx("div", { style: frameStyle, children: _jsxs("div", { style: wrapperStyle, role: "application", "aria-label": "iOS simulator \u2014 click to focus; tap, drag to swipe, type to send keys", tabIndex: 0, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onKeyDown: handleKeyDown, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: [videoSupported && (_jsx("canvas", { ref: setCanvas, style: canvasStyle, "aria-label": "iOS Simulator" })), !videoSupported ? (_jsxs("div", { style: placeholderStyle, children: [_jsx("span", { style: { fontWeight: 500 }, children: "Live video isn't supported here" }), _jsx("span", { style: { fontSize: "11px", textAlign: "center", maxWidth: "80%" }, children: "This browser can't decode the stream (WebCodecs unavailable). Try a recent Chrome, Edge, or Safari 16.4+." })] })) : (!hasFrame && (_jsxs("div", { style: placeholderStyle, children: [_jsx("span", { style: { fontWeight: 500 }, children: "Waiting for the simulator" }), _jsx("div", { style: { display: "flex", gap: "6px" }, children: [0, 1, 2].map((i) => (_jsx("span", { style: {
|
|
196
205
|
width: 6,
|
|
197
206
|
height: 6,
|
|
198
207
|
borderRadius: "50%",
|
|
199
208
|
backgroundColor: "currentColor",
|
|
200
209
|
animation: `expo-pulse 1.2s ease-in-out ${i * 0.2}s infinite`,
|
|
201
|
-
} }, i))) })] })), ripple && (_jsx("span", { style: {
|
|
210
|
+
} }, i))) })] }))), ripple && (_jsx("span", { style: {
|
|
202
211
|
position: "absolute",
|
|
203
212
|
left: ripple.x - 14,
|
|
204
213
|
top: ripple.y - 14,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface FrameSize {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Owns a `VideoDecoder` and paints decoded frames onto a caller-provided
|
|
7
|
+
* `<canvas>`. Feed it raw chunks via {@link push}; attach the canvas with
|
|
8
|
+
* {@link setCanvas}. One instance per session; `dispose()` when done.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SimulatorDecoder {
|
|
11
|
+
/** Whether this environment can decode the stream at all (needs WebCodecs). */
|
|
12
|
+
static isSupported(): boolean;
|
|
13
|
+
private decoder;
|
|
14
|
+
private canvas;
|
|
15
|
+
private ctx;
|
|
16
|
+
private size;
|
|
17
|
+
private onResizeCb;
|
|
18
|
+
private nextTimestamp;
|
|
19
|
+
private awaitingKeyframe;
|
|
20
|
+
private disposed;
|
|
21
|
+
setCanvas(canvas: HTMLCanvasElement | null): void;
|
|
22
|
+
onResize(cb: ((size: FrameSize) => void) | null): void;
|
|
23
|
+
get frameSize(): FrameSize | null;
|
|
24
|
+
push(chunk: ArrayBuffer): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
private configure;
|
|
27
|
+
private decode;
|
|
28
|
+
private paintFrame;
|
|
29
|
+
private paintJpeg;
|
|
30
|
+
private updateSize;
|
|
31
|
+
private onDecoderError;
|
|
32
|
+
private closeDecoder;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=decoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../src/decoder.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,WAAW,IAAI,OAAO;IAI7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,UAAU,CAA4C;IAG9D,OAAO,CAAC,aAAa,CAAK;IAG1B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAS;IAEzB,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IASjD,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAItD,IAAI,SAAS,IAAI,SAAS,GAAG,IAAI,CAEhC;IAED,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAqB9B,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,YAAY;CAWrB"}
|
package/dist/decoder.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// WebCodecs H.264 decoder for the simulator stream. The server forwards
|
|
2
|
+
// `baguette serve`'s AVCC chunks verbatim: each is `[tag byte][payload]` with
|
|
3
|
+
// 0x01 avcC description — feed to VideoDecoder.configure
|
|
4
|
+
// 0x02 keyframe (IDR)
|
|
5
|
+
// 0x03 delta frame
|
|
6
|
+
// 0x04 standalone JPEG — painted immediately for an instant first frame
|
|
7
|
+
// We decode H.264 with a VideoDecoder and paint each frame onto a <canvas>.
|
|
8
|
+
const TAG_DESCRIPTION = 0x01;
|
|
9
|
+
const TAG_KEYFRAME = 0x02;
|
|
10
|
+
const TAG_DELTA = 0x03;
|
|
11
|
+
const TAG_JPEG_SEED = 0x04;
|
|
12
|
+
/**
|
|
13
|
+
* Owns a `VideoDecoder` and paints decoded frames onto a caller-provided
|
|
14
|
+
* `<canvas>`. Feed it raw chunks via {@link push}; attach the canvas with
|
|
15
|
+
* {@link setCanvas}. One instance per session; `dispose()` when done.
|
|
16
|
+
*/
|
|
17
|
+
export class SimulatorDecoder {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.decoder = null;
|
|
20
|
+
this.canvas = null;
|
|
21
|
+
this.ctx = null;
|
|
22
|
+
this.size = null;
|
|
23
|
+
this.onResizeCb = null;
|
|
24
|
+
// Monotonic, strictly-increasing microsecond timestamps. The values are
|
|
25
|
+
// arbitrary (we render immediately) but EncodedVideoChunk requires them.
|
|
26
|
+
this.nextTimestamp = 0;
|
|
27
|
+
// Until the first keyframe arrives after (re)configuring, deltas reference
|
|
28
|
+
// frames we never decoded — skip them to avoid decoder errors.
|
|
29
|
+
this.awaitingKeyframe = true;
|
|
30
|
+
this.disposed = false;
|
|
31
|
+
}
|
|
32
|
+
/** Whether this environment can decode the stream at all (needs WebCodecs). */
|
|
33
|
+
static isSupported() {
|
|
34
|
+
return typeof globalThis !== "undefined" && "VideoDecoder" in globalThis;
|
|
35
|
+
}
|
|
36
|
+
setCanvas(canvas) {
|
|
37
|
+
this.canvas = canvas;
|
|
38
|
+
this.ctx = canvas ? canvas.getContext("2d") : null;
|
|
39
|
+
if (canvas && this.size) {
|
|
40
|
+
canvas.width = this.size.width;
|
|
41
|
+
canvas.height = this.size.height;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
onResize(cb) {
|
|
45
|
+
this.onResizeCb = cb;
|
|
46
|
+
}
|
|
47
|
+
get frameSize() {
|
|
48
|
+
return this.size;
|
|
49
|
+
}
|
|
50
|
+
push(chunk) {
|
|
51
|
+
if (this.disposed || chunk.byteLength === 0)
|
|
52
|
+
return;
|
|
53
|
+
const bytes = new Uint8Array(chunk);
|
|
54
|
+
const tag = bytes[0];
|
|
55
|
+
const payload = bytes.subarray(1);
|
|
56
|
+
switch (tag) {
|
|
57
|
+
case TAG_DESCRIPTION:
|
|
58
|
+
this.configure(payload);
|
|
59
|
+
break;
|
|
60
|
+
case TAG_JPEG_SEED:
|
|
61
|
+
this.paintJpeg(payload);
|
|
62
|
+
break;
|
|
63
|
+
case TAG_KEYFRAME:
|
|
64
|
+
this.decode(payload, "key");
|
|
65
|
+
break;
|
|
66
|
+
case TAG_DELTA:
|
|
67
|
+
this.decode(payload, "delta");
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
dispose() {
|
|
72
|
+
this.disposed = true;
|
|
73
|
+
this.closeDecoder();
|
|
74
|
+
this.canvas = null;
|
|
75
|
+
this.ctx = null;
|
|
76
|
+
this.onResizeCb = null;
|
|
77
|
+
}
|
|
78
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
79
|
+
configure(description) {
|
|
80
|
+
if (!SimulatorDecoder.isSupported() || description.byteLength < 4)
|
|
81
|
+
return;
|
|
82
|
+
this.closeDecoder();
|
|
83
|
+
const decoder = new VideoDecoder({
|
|
84
|
+
output: (frame) => this.paintFrame(frame),
|
|
85
|
+
error: (err) => this.onDecoderError(err),
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
decoder.configure({
|
|
89
|
+
codec: codecFromAvcC(description),
|
|
90
|
+
description,
|
|
91
|
+
optimizeForLatency: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Unsupported profile/level on this platform — leave video unsupported.
|
|
96
|
+
decoder.close();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.decoder = decoder;
|
|
100
|
+
this.awaitingKeyframe = true;
|
|
101
|
+
}
|
|
102
|
+
decode(data, type) {
|
|
103
|
+
const decoder = this.decoder;
|
|
104
|
+
if (!decoder || decoder.state !== "configured")
|
|
105
|
+
return;
|
|
106
|
+
if (type === "key") {
|
|
107
|
+
this.awaitingKeyframe = false;
|
|
108
|
+
}
|
|
109
|
+
else if (this.awaitingKeyframe) {
|
|
110
|
+
return; // can't decode a delta before its keyframe
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
decoder.decode(new EncodedVideoChunk({
|
|
114
|
+
type,
|
|
115
|
+
timestamp: this.nextTimestamp++,
|
|
116
|
+
data,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A bad/out-of-order chunk — wait for the next keyframe to resync.
|
|
121
|
+
this.awaitingKeyframe = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
paintFrame(frame) {
|
|
125
|
+
const width = frame.displayWidth;
|
|
126
|
+
const height = frame.displayHeight;
|
|
127
|
+
this.updateSize(width, height);
|
|
128
|
+
if (this.canvas && this.ctx) {
|
|
129
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
130
|
+
}
|
|
131
|
+
frame.close();
|
|
132
|
+
}
|
|
133
|
+
paintJpeg(jpeg) {
|
|
134
|
+
if (jpeg.byteLength === 0 || typeof createImageBitmap !== "function")
|
|
135
|
+
return;
|
|
136
|
+
// Copy into a fresh buffer so the Blob owns its bytes.
|
|
137
|
+
const blob = new Blob([jpeg.slice()], { type: "image/jpeg" });
|
|
138
|
+
createImageBitmap(blob)
|
|
139
|
+
.then((bitmap) => {
|
|
140
|
+
this.updateSize(bitmap.width, bitmap.height);
|
|
141
|
+
if (this.canvas && this.ctx) {
|
|
142
|
+
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
143
|
+
}
|
|
144
|
+
bitmap.close();
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {
|
|
147
|
+
/* a stray/partial seed — ignore */
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
updateSize(width, height) {
|
|
151
|
+
if (width === 0 || height === 0)
|
|
152
|
+
return;
|
|
153
|
+
if (this.size && this.size.width === width && this.size.height === height)
|
|
154
|
+
return;
|
|
155
|
+
this.size = { width, height };
|
|
156
|
+
if (this.canvas) {
|
|
157
|
+
this.canvas.width = width;
|
|
158
|
+
this.canvas.height = height;
|
|
159
|
+
}
|
|
160
|
+
this.onResizeCb?.(this.size);
|
|
161
|
+
}
|
|
162
|
+
onDecoderError(_err) {
|
|
163
|
+
// Reset and wait for the next server-forced keyframe to recover.
|
|
164
|
+
this.awaitingKeyframe = true;
|
|
165
|
+
}
|
|
166
|
+
closeDecoder() {
|
|
167
|
+
const decoder = this.decoder;
|
|
168
|
+
this.decoder = null;
|
|
169
|
+
if (decoder && decoder.state !== "closed") {
|
|
170
|
+
try {
|
|
171
|
+
decoder.close();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* already closed */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Derive the WebCodecs codec string from an avcC configuration record. Bytes
|
|
181
|
+
* 1–3 are AVCProfileIndication / profile_compatibility / AVCLevelIndication,
|
|
182
|
+
* which form the `avc1.PPCCLL` suffix VideoDecoder.configure expects.
|
|
183
|
+
*/
|
|
184
|
+
function codecFromAvcC(avcc) {
|
|
185
|
+
const hex = (b) => b.toString(16).padStart(2, "0");
|
|
186
|
+
return `avc1.${hex(avcc[1])}${hex(avcc[2])}${hex(avcc[3])}`;
|
|
187
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { SessionViewerProps } from "./components/SessionViewer.js";
|
|
|
5
5
|
export { SessionViewer } from "./components/SessionViewer.js";
|
|
6
6
|
export { SimulatorScreen } from "./components/SimulatorScreen.js";
|
|
7
7
|
export { StatusBadge } from "./components/StatusBadge.js";
|
|
8
|
-
export type {
|
|
8
|
+
export type { FrameSize } from "./decoder.js";
|
|
9
|
+
export { SimulatorDecoder } from "./decoder.js";
|
|
10
|
+
export type { ClientEventMap, ListSimulatorsResponse, LogEntry, LogLevel, SessionDetail, SessionLogs, SessionStatus, SimulatorButton, SimulatorDevice, SimulatorInput, SimulatorInputResponse, SimulatorKeyCode, SimulatorKeyModifier, UseExpoCiSessionOptions, UseExpoCiSessionResult, WsConnectedMessage, WsErrorMessage, WsLogMessage, WsMessage, WsStatusMessage, } from "./types.js";
|
|
9
11
|
export { useExpoCiSession } from "./useExpoCiSession.js";
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,YAAY,EACV,cAAc,EACd,sBAAsB,EACtB,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,SAAS,EACT,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,5 +5,7 @@ export { LogConsole } from "./components/LogConsole.js";
|
|
|
5
5
|
export { SessionViewer } from "./components/SessionViewer.js";
|
|
6
6
|
export { SimulatorScreen } from "./components/SimulatorScreen.js";
|
|
7
7
|
export { StatusBadge } from "./components/StatusBadge.js";
|
|
8
|
+
// Decoder (advanced: drive your own canvas)
|
|
9
|
+
export { SimulatorDecoder } from "./decoder.js";
|
|
8
10
|
// Hook
|
|
9
11
|
export { useExpoCiSession } from "./useExpoCiSession.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -20,13 +20,6 @@ export interface WsLogMessage {
|
|
|
20
20
|
message: string;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
export interface WsVideoFrameMessage {
|
|
24
|
-
type: "video-frame";
|
|
25
|
-
sessionId: string;
|
|
26
|
-
timestamp: string;
|
|
27
|
-
/** base64 JPEG with NO data-URI prefix */
|
|
28
|
-
data: string;
|
|
29
|
-
}
|
|
30
23
|
export interface WsStatusMessage {
|
|
31
24
|
type: "status";
|
|
32
25
|
sessionId: string;
|
|
@@ -43,13 +36,16 @@ export interface WsErrorMessage {
|
|
|
43
36
|
message: string;
|
|
44
37
|
};
|
|
45
38
|
}
|
|
46
|
-
|
|
39
|
+
/** JSON messages pushed down the WebSocket. Live video is sent as binary
|
|
40
|
+
* frames instead — see the `video-chunk` client event. */
|
|
41
|
+
export type WsMessage = WsConnectedMessage | WsLogMessage | WsStatusMessage | WsErrorMessage;
|
|
47
42
|
export interface ClientEventMap {
|
|
48
43
|
open: undefined;
|
|
49
44
|
close: undefined;
|
|
50
45
|
connected: WsConnectedMessage;
|
|
51
46
|
log: WsLogMessage;
|
|
52
|
-
|
|
47
|
+
/** One raw encoded H.264/AVCC chunk (`[tag][payload]`) for the decoder. */
|
|
48
|
+
"video-chunk": ArrayBuffer;
|
|
53
49
|
status: WsStatusMessage;
|
|
54
50
|
error: WsErrorMessage;
|
|
55
51
|
}
|
|
@@ -65,8 +61,16 @@ export interface UseExpoCiSessionOptions {
|
|
|
65
61
|
export interface UseExpoCiSessionResult {
|
|
66
62
|
status: SessionStatus | null;
|
|
67
63
|
logs: LogEntry[];
|
|
68
|
-
/**
|
|
69
|
-
|
|
64
|
+
/** Attach the `<canvas>` the decoder paints the live H.264 stream onto.
|
|
65
|
+
* Pass via a ref callback; pass `null` on unmount. */
|
|
66
|
+
attachCanvas: (canvas: HTMLCanvasElement | null) => void;
|
|
67
|
+
/** Whether this browser can decode the stream (WebCodecs available). */
|
|
68
|
+
videoSupported: boolean;
|
|
69
|
+
/** Natural pixel size of the decoded video, once the first frame paints. */
|
|
70
|
+
frameSize: {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
} | null;
|
|
70
74
|
connected: boolean;
|
|
71
75
|
error: string | null;
|
|
72
76
|
sendInput: (input: SimulatorInput) => Promise<import("@alivelabs/expo-orchestrator-schemas").SimulatorInputResponse>;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAE/F,YAAY,EACV,sBAAsB,EACtB,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAE9C,0CAA0C;AAC1C,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC;AAE3B,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAMD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,KAAK,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,sCAAsC,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3F;AAED,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAE/F,YAAY,EACV,sBAAsB,EACtB,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAE9C,0CAA0C;AAC1C,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC;AAE3B,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAMD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,KAAK,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,sCAAsC,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3F;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,MAAM,EAAE,aAAa,CAAA;KAAE,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3B;AAED;2DAC2D;AAC3D,MAAM,MAAM,SAAS,GAAG,kBAAkB,GAAG,YAAY,GAAG,eAAe,GAAG,cAAc,CAAC;AAI7F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,kBAAkB,CAAC;IAC9B,GAAG,EAAE,YAAY,CAAC;IAClB,2EAA2E;IAC3E,aAAa,EAAE,WAAW,CAAC;IAC3B,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,cAAc,CAAC;CACvB;AAID,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB;2DACuD;IACvD,YAAY,EAAE,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,wEAAwE;IACxE,cAAc,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,CACT,KAAK,EAAE,cAAc,KAClB,OAAO,CAAC,OAAO,sCAAsC,EAAE,sBAAsB,CAAC,CAAC;IACpF,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useExpoCiSession.d.ts","sourceRoot":"","sources":["../src/useExpoCiSession.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useExpoCiSession.d.ts","sourceRoot":"","sources":["../src/useExpoCiSession.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAIpB,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,WAAkB,EAClB,OAA0B,GAC3B,EAAE,uBAAuB,GAAG,sBAAsB,CA8HlD"}
|
package/dist/useExpoCiSession.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { ExpoCiClient } from "./client.js";
|
|
3
|
+
import { SimulatorDecoder } from "./decoder.js";
|
|
3
4
|
const DEFAULT_MAX_LOGS = 2000;
|
|
4
5
|
export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = true, maxLogs = DEFAULT_MAX_LOGS, }) {
|
|
5
6
|
const [status, setStatus] = useState(null);
|
|
6
7
|
const [logs, setLogs] = useState([]);
|
|
7
|
-
const [
|
|
8
|
+
const [frameSize, setFrameSize] = useState(null);
|
|
8
9
|
const [connected, setConnected] = useState(false);
|
|
9
10
|
const [error, setError] = useState(null);
|
|
11
|
+
const videoSupported = useMemo(() => SimulatorDecoder.isSupported(), []);
|
|
10
12
|
// We keep the client in a ref so reconnect() can call client.connect()
|
|
11
13
|
// without causing a re-render or stale closure issues.
|
|
12
14
|
const clientRef = useRef(null);
|
|
15
|
+
// One decoder per hook instance; lives across reconnects within a session.
|
|
16
|
+
const decoderRef = useRef(null);
|
|
17
|
+
if (decoderRef.current === null) {
|
|
18
|
+
decoderRef.current = new SimulatorDecoder();
|
|
19
|
+
decoderRef.current.onResize(setFrameSize);
|
|
20
|
+
}
|
|
21
|
+
// Stable callback to wire the rendering <canvas> into the decoder.
|
|
22
|
+
const attachCanvas = useCallback((canvas) => {
|
|
23
|
+
decoderRef.current?.setCanvas(canvas);
|
|
24
|
+
}, []);
|
|
13
25
|
// Stable reconnect callback
|
|
14
26
|
const reconnect = useCallback(() => {
|
|
15
27
|
clientRef.current?.connect();
|
|
@@ -22,6 +34,13 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
|
|
|
22
34
|
}
|
|
23
35
|
return client.sendInput(input);
|
|
24
36
|
}, []);
|
|
37
|
+
// Tear the decoder down only on unmount — it is reused across reconnects.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
return () => {
|
|
40
|
+
decoderRef.current?.dispose();
|
|
41
|
+
decoderRef.current = null;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
25
44
|
useEffect(() => {
|
|
26
45
|
const client = new ExpoCiClient({ baseUrl, sessionId, apiToken });
|
|
27
46
|
clientRef.current = client;
|
|
@@ -56,8 +75,8 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
|
|
|
56
75
|
return next.length > maxLogs ? next.slice(next.length - maxLogs) : next;
|
|
57
76
|
});
|
|
58
77
|
});
|
|
59
|
-
const offFrame = client.on("video-
|
|
60
|
-
|
|
78
|
+
const offFrame = client.on("video-chunk", (chunk) => {
|
|
79
|
+
decoderRef.current?.push(chunk);
|
|
61
80
|
});
|
|
62
81
|
const offStatus = client.on("status", (msg) => {
|
|
63
82
|
setStatus(msg.data.status);
|
|
@@ -80,5 +99,15 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
|
|
|
80
99
|
};
|
|
81
100
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
101
|
}, [baseUrl, sessionId, apiToken, autoConnect, maxLogs]);
|
|
83
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
status,
|
|
104
|
+
logs,
|
|
105
|
+
attachCanvas,
|
|
106
|
+
videoSupported,
|
|
107
|
+
frameSize,
|
|
108
|
+
connected,
|
|
109
|
+
error,
|
|
110
|
+
sendInput,
|
|
111
|
+
reconnect,
|
|
112
|
+
};
|
|
84
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alivelabs/expo-orchestrator-react-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React client for Expo CI Orchestrator — streaming logs, live simulator video, and interactive controls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@alivelabs/expo-orchestrator-schemas": "^0.
|
|
33
|
+
"@alivelabs/expo-orchestrator-schemas": "^0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=18.0.0",
|