@codefilm/recorder 3.0.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 +1553 -0
- package/dist/code-film.css +1 -0
- package/dist/core/FilmRecorder.d.ts +192 -0
- package/dist/core/FilmRecorder.d.ts.map +1 -0
- package/dist/core/FilmRecorder.js +1504 -0
- package/dist/core/agent.d.ts +27 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +258 -0
- package/dist/core/app-script.d.ts +232 -0
- package/dist/core/app-script.d.ts.map +1 -0
- package/dist/core/app-script.js +916 -0
- package/dist/core/iframe-bridge.d.ts +34 -0
- package/dist/core/iframe-bridge.d.ts.map +1 -0
- package/dist/core/iframe-bridge.js +97 -0
- package/dist/core/recording-overlays.d.ts +12 -0
- package/dist/core/recording-overlays.d.ts.map +1 -0
- package/dist/core/recording-overlays.js +237 -0
- package/dist/core/types.d.ts +256 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.umd.cjs +156 -0
- package/dist/react/RegionRecorder.d.ts +7 -0
- package/dist/react/RegionRecorder.d.ts.map +1 -0
- package/dist/react/RegionRecorder.js +190 -0
- package/dist/react/useFilmRecorder.d.ts +11 -0
- package/dist/react/useFilmRecorder.d.ts.map +1 -0
- package/dist/react/useFilmRecorder.js +61 -0
- package/dist/vanilla/RegionRecorderUI.d.ts +21 -0
- package/dist/vanilla/RegionRecorderUI.d.ts.map +1 -0
- package/dist/vanilla/RegionRecorderUI.js +128 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,1553 @@
|
|
|
1
|
+
# code-film 🎬
|
|
2
|
+
|
|
3
|
+
Cinematic screen region recorder for React and vanilla JavaScript applications. Capture perfect crop areas, dynamic focal zoom boundaries, custom aspect ratios, and export high-quality WebM/MP4 recordings seamlessly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🎯 **Cinematic Cropping**: Draw or snap a recording frame to strict aspect ratios (e.g., 16:9, 9:16, 4:5) without video stretching.
|
|
10
|
+
- 🔍 **Automatic Focal Zoom**: Smooth, animated focal zooming (`requestAnimationFrame` LERP easing) centered on mouse hold.
|
|
11
|
+
- ⚛️ **Zero-Dependency React Bindings**: Drop-in `<RegionRecorder>` component and headless `useFilmRecorder` hook.
|
|
12
|
+
- 🎨 **Fully Customizable & Scoped CSS**: Clean modern glassmorphism styled with theme-friendly CSS custom properties (prefixed with `cfr-`).
|
|
13
|
+
- 🛡️ **Iframe Friendly**: Fully supports tab capture and canvas cropping across cross-origin iframe boundaries.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install the package via npm, yarn, or bun:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @codefilm/recorder
|
|
23
|
+
# or
|
|
24
|
+
yarn add @codefilm/recorder
|
|
25
|
+
# or
|
|
26
|
+
bun add @codefilm/recorder
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## React Integration
|
|
32
|
+
|
|
33
|
+
### 1. Quick Start (Drop-in UI Component)
|
|
34
|
+
|
|
35
|
+
Import the recorder component and load the companion stylesheet. This mounts the full cinematic settings bar, coordinate grid, and crop outline.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import React from "react";
|
|
39
|
+
import { RegionRecorder } from "@codefilm/recorder";
|
|
40
|
+
import "@codefilm/recorder/style.css";
|
|
41
|
+
|
|
42
|
+
export default function App() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="app-container">
|
|
45
|
+
{/* Cinematic Screen Recorder Component */}
|
|
46
|
+
<RegionRecorder
|
|
47
|
+
options={{
|
|
48
|
+
fps: 60,
|
|
49
|
+
autoZoom: true,
|
|
50
|
+
showOutline: false, // Hidden by default so it doesn't appear in the output video
|
|
51
|
+
showGrid: true,
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<main>
|
|
56
|
+
<h1>My Premium Application</h1>
|
|
57
|
+
<p>Record your screen dynamically using focal zoom transitions.</p>
|
|
58
|
+
</main>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Headless Integration (Hook API)
|
|
65
|
+
|
|
66
|
+
If you want to build a completely custom UI (e.g. customized control buttons, side panels, or branding) while leveraging the underlying capture logic:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import React from "react";
|
|
70
|
+
import { useFilmRecorder } from "@codefilm/recorder";
|
|
71
|
+
import "@codefilm/recorder/style.css"; // Optional, or write your own styles
|
|
72
|
+
|
|
73
|
+
export default function CustomRecorder() {
|
|
74
|
+
const { recorder, state } = useFilmRecorder({
|
|
75
|
+
fps: 60,
|
|
76
|
+
autoZoom: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!state || !recorder) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="custom-dashboard">
|
|
83
|
+
<div className="status">Status: {state.recording ? "🔴 Recording" : "⚪ Idle"}</div>
|
|
84
|
+
|
|
85
|
+
<div className="buttons">
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => (state.recording ? recorder.stopRecording() : recorder.startRecording())}
|
|
88
|
+
>
|
|
89
|
+
{state.recording ? "Stop Capture" : "Start Capture"}
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
<button onClick={() => recorder.resetFullScreen()}>Zoom Full Screen</button>
|
|
93
|
+
|
|
94
|
+
<button onClick={() => recorder.zoomTop()}>Zoom Top</button>
|
|
95
|
+
|
|
96
|
+
<button onClick={() => recorder.zoomBottom()}>Zoom Bottom</button>
|
|
97
|
+
|
|
98
|
+
<button onClick={() => recorder.zoomToMouse()}>Zoom Focal Mouse</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Recording Artifacts
|
|
106
|
+
|
|
107
|
+
By default, recordings still auto-download when stopped. For custom integrations, disable the download and receive the artifact directly:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const recorder = new FilmRecorder({
|
|
111
|
+
autoDownload: false,
|
|
112
|
+
includeTimelineMetadata: true,
|
|
113
|
+
onRecordingStop: (artifact) => {
|
|
114
|
+
console.log(artifact.blob, artifact.url, artifact.mimeType, artifact.filename);
|
|
115
|
+
console.log(artifact.timeline);
|
|
116
|
+
console.log(artifact.webm);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const latest = recorder.getLastRecording();
|
|
121
|
+
|
|
122
|
+
// Revoke the object URL when your app no longer needs it.
|
|
123
|
+
recorder.clearLastRecording();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`includeTimelineMetadata` returns container-agnostic timing data on the artifact. It is not muxed into the WebM container by the browser SDK, but it gives audio generators and renderer pipelines scene, action, marker, and timing data for synchronization:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
artifact.timeline?.markers.forEach((marker) => {
|
|
130
|
+
console.log(marker.timeMs, marker.name, marker.data);
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Ignoring DOM Elements During Recording
|
|
135
|
+
|
|
136
|
+
Use `ignoreSelector` to hide matching page elements while recording is active. This is useful for controls, badges, or helper UI that should stay out of the captured video.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const recorder = new FilmRecorder({
|
|
140
|
+
ignoreSelector: "[data-code-film-ignore]",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
recorder.setIgnoreSelector(".recording-toolbar");
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Recording Errors and Prepared Capture
|
|
147
|
+
|
|
148
|
+
`startRecording()` returns `true` after MediaRecorder starts, returns `false` if recording is already active, and re-throws browser capture errors such as denied screen-share permission or missing user activation.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const recorder = new FilmRecorder({
|
|
152
|
+
onRecordingError: (err) => {
|
|
153
|
+
console.error("Recording failed", err);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await recorder.startRecording();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(recorder.getLastRecordingError());
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For AI or agent flows, call `prepareCapture()` directly from a user click to satisfy browser activation, then start the actual MediaRecorder later so the LLM/script-generation wait is not recorded:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
async function handleGenerateDemo() {
|
|
168
|
+
await recorder.prepareCapture(); // prompts immediately from the user gesture
|
|
169
|
+
|
|
170
|
+
const script = await generateFilmScript();
|
|
171
|
+
|
|
172
|
+
await recorder.startRecording(); // uses the prepared stream
|
|
173
|
+
await recorder.runFilmScript(script);
|
|
174
|
+
recorder.stopRecording();
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Element Selector Recording 🎯
|
|
181
|
+
|
|
182
|
+
The SDK allows you to track and record specific DOM elements by their CSS selector (e.g., `#editor`, `.card-active`). The capture box automatically wraps around the target element, keeping its center as the center of the recording viewport while maintaining your chosen aspect ratio.
|
|
183
|
+
|
|
184
|
+
The camera will dynamically pan and zoom in real-time to keep the element perfectly framed if it moves, resizes, or if the page scrolls.
|
|
185
|
+
|
|
186
|
+
### Programmatic Usage
|
|
187
|
+
|
|
188
|
+
You can set, change, or clear target elements dynamically:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Focus on a specific div with custom element padding
|
|
192
|
+
recorder.setElementSelector("#my-element");
|
|
193
|
+
recorder.setElementPadding(1.2); // 20% margin around the element
|
|
194
|
+
|
|
195
|
+
// Stop tracking and zoom back to full screen
|
|
196
|
+
recorder.setElementSelector(null);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
To keep the recording box at a fixed size while it moves between targets, opt into the
|
|
200
|
+
smallest 16:9 region:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const recorder = new FilmRecorder({
|
|
204
|
+
fixedRecordingRegion: "smallest-16:9",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
recorder.setElementSelector(".thread-thinking-status");
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
With `fixedRecordingRegion`, the SDK centers the fixed 16:9 capture box on the target
|
|
211
|
+
instead of expanding the box to fit the full element.
|
|
212
|
+
|
|
213
|
+
### Iframe Recording Bridge
|
|
214
|
+
|
|
215
|
+
When the recorder lives in a parent page but the app being controlled is inside an iframe,
|
|
216
|
+
install the SDK bridge on both sides. The host owns the `FilmRecorder`; the child forwards
|
|
217
|
+
shortcuts and optional explicit commands with `postMessage`.
|
|
218
|
+
|
|
219
|
+
Parent page:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { FilmRecorder, createRecorderIframeHost } from "@codefilm/recorder";
|
|
223
|
+
|
|
224
|
+
const recorder = new FilmRecorder({
|
|
225
|
+
elementSelector: "#recording-region",
|
|
226
|
+
autoDownload: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const bridge = createRecorderIframeHost(recorder, {
|
|
230
|
+
allowedOrigins: ["http://127.0.0.1:5173"],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
bridge.start();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Iframe child:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { createRecorderIframeClient } from "@codefilm/recorder";
|
|
240
|
+
|
|
241
|
+
const bridge = createRecorderIframeClient({
|
|
242
|
+
targetOrigin: "http://127.0.0.1:10000",
|
|
243
|
+
shortcuts: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
bridge.start();
|
|
247
|
+
|
|
248
|
+
bridge.sendCommand("focus-mouse");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Supported commands are `toggle-recording`, `start-recording`, `stop-recording`,
|
|
252
|
+
`focus-mouse`, `reset-full`, and `toggle-mouse-pause`. If `allowedOrigins` is set on
|
|
253
|
+
the host, messages from any other iframe origin are ignored.
|
|
254
|
+
|
|
255
|
+
### Recording Overlays
|
|
256
|
+
|
|
257
|
+
Use SDK overlays for recording-only UI such as a presenter camera, captions, labels, or
|
|
258
|
+
watermarks. Built-in camera overlays are composited into the final recorder canvas, so they
|
|
259
|
+
remain visible even when recording an element selector or a fixed recording region.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { FilmRecorder, cameraOverlay } from "@codefilm/recorder";
|
|
263
|
+
|
|
264
|
+
const recorder = new FilmRecorder({
|
|
265
|
+
elementSelector: ".app-shell",
|
|
266
|
+
includeTimelineMetadata: true,
|
|
267
|
+
overlays: [
|
|
268
|
+
cameraOverlay({
|
|
269
|
+
id: "presenter-camera",
|
|
270
|
+
enabled: true,
|
|
271
|
+
position: { x: 24, y: 24 },
|
|
272
|
+
size: 168,
|
|
273
|
+
shape: "circle",
|
|
274
|
+
draggable: true,
|
|
275
|
+
resizable: true,
|
|
276
|
+
mirror: true,
|
|
277
|
+
}),
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
You can also create and control an overlay imperatively:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const camera = recorder.createCameraOverlay({
|
|
286
|
+
id: "presenter-camera",
|
|
287
|
+
size: 180,
|
|
288
|
+
position: "top-left",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await camera.requestPermission();
|
|
292
|
+
await camera.show();
|
|
293
|
+
camera.setSize(220);
|
|
294
|
+
camera.moveTo({ x: 32, y: 32 });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
If camera permission is denied, the SDK draws a visible fallback tile instead of leaving a
|
|
298
|
+
blank overlay. Camera tracks stop when the overlay is hidden, destroyed, or when the recorder
|
|
299
|
+
is disposed.
|
|
300
|
+
|
|
301
|
+
Register custom DOM overlays when the host app owns the element:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const caption = recorder.registerOverlay({
|
|
305
|
+
id: "caption",
|
|
306
|
+
element: document.querySelector("#recording-caption") as HTMLElement,
|
|
307
|
+
includeInCapture: true,
|
|
308
|
+
trackTimeline: true,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
caption.show();
|
|
312
|
+
caption.moveTo("bottom-left");
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
When `includeTimelineMetadata` is enabled, overlay changes add timeline markers named
|
|
316
|
+
`overlay:shown`, `overlay:hidden`, `overlay:moved`, `overlay:resized`, and `overlay:error`.
|
|
317
|
+
|
|
318
|
+
### Initialization Options
|
|
319
|
+
|
|
320
|
+
You can also specify the target element selector directly when initializing:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
const recorder = new FilmRecorder({
|
|
324
|
+
elementSelector: "#main-content",
|
|
325
|
+
elementPadding: 1.15, // 15% margin around the element
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### React Integration
|
|
330
|
+
|
|
331
|
+
When using the React `<RegionRecorder>` component or `useFilmRecorder` hook, pass the selector and padding via the options prop. It will automatically update in response to state changes:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import React, { useState } from "react";
|
|
335
|
+
import { RegionRecorder } from "@codefilm/recorder";
|
|
336
|
+
|
|
337
|
+
export default function MyRecorder() {
|
|
338
|
+
const [selector, setSelector] = useState<string | null>("#element-1");
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div>
|
|
342
|
+
<RegionRecorder
|
|
343
|
+
options={{
|
|
344
|
+
elementSelector: selector,
|
|
345
|
+
elementPadding: 1.2,
|
|
346
|
+
}}
|
|
347
|
+
/>
|
|
348
|
+
<button onClick={() => setSelector("#element-1")}>Track Element 1</button>
|
|
349
|
+
<button onClick={() => setSelector("#element-2")}>Track Element 2</button>
|
|
350
|
+
<button onClick={() => setSelector(null)}>Full Screen</button>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Simulated Mouse Tours 🖱️
|
|
357
|
+
|
|
358
|
+
You can programmatically trigger a simulated mouse movement and zoom tour to target elements. This creates a virtual cursor overlay on the page, slides it smoothly to the destination, performs a visual ripple click-hold, triggers the zoom-in, pauses, and then zooms back out.
|
|
359
|
+
|
|
360
|
+
### Programmatic Usage
|
|
361
|
+
|
|
362
|
+
Call the `playPointerTour` method at any time during a recording:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Move pointer to #my-element, perform click-hold to zoom in, hold for 2.5s, and zoom out
|
|
366
|
+
await recorder.playPointerTour("#my-element", {
|
|
367
|
+
moveDuration: 1200, // Duration of pointer movement in ms
|
|
368
|
+
holdDuration: 2500, // Zoom hold duration in ms
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Automatic Recording Tours
|
|
373
|
+
|
|
374
|
+
You can configure the tour to play automatically when recording starts by passing the `movePointerToSelector` option:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const recorder = new FilmRecorder({
|
|
378
|
+
movePointerToSelector: "#my-element",
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
When recording begins, the virtual mouse cursor will automatically move to `#my-element`, zoom in, hold, and zoom out.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## AI Agent Prompt Automation & Script Runner 🤖
|
|
387
|
+
|
|
388
|
+
You can automate complete cinematic recording tours directly from text prompts or structured film scripts using the **OpenAI Agents SDK** integration or the offline **Deterministic Script Runner**.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
### 1. Proxy-First Runner Pattern (Recommended for Browser Apps)
|
|
393
|
+
|
|
394
|
+
To avoid exposing your OpenAI API keys on the client-side, the SDK provides a helper `createRecorderAgentRunner` configured for server-side proxies.
|
|
395
|
+
|
|
396
|
+
#### Installation
|
|
397
|
+
```bash
|
|
398
|
+
npm install @openai/agents openai zod
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
> [!IMPORTANT]
|
|
402
|
+
> **Zod Version Resolution**:
|
|
403
|
+
> The `code-film` SDK defines its built-in tool schemas using static, raw JSON Schema objects to be completely resilient against Zod runtime versions. If you are defining custom tools or using Zod, avoid aliasing or forcing deduplication of `zod` in your bundler configuration (e.g. Vite or Webpack) unless the versions are known to be compatible.
|
|
404
|
+
|
|
405
|
+
#### React Implementation
|
|
406
|
+
```tsx
|
|
407
|
+
import React, { useState } from "react";
|
|
408
|
+
import { useFilmRecorder, createRecorderAgentRunner } from "@codefilm/recorder";
|
|
409
|
+
|
|
410
|
+
export default function AgentAutomationDemo() {
|
|
411
|
+
const { recorder, state, tourState, currentScene, stopTour } = useFilmRecorder({
|
|
412
|
+
fps: 30,
|
|
413
|
+
onTourStart: () => console.log("Tour started!"),
|
|
414
|
+
onReadyToEnd: () => setStatus("Finishing closing shot..."),
|
|
415
|
+
onTourStop: () => console.log("Tour finished!"),
|
|
416
|
+
});
|
|
417
|
+
const [status, setStatus] = useState("Idle");
|
|
418
|
+
|
|
419
|
+
const handleRunTour = async () => {
|
|
420
|
+
if (!recorder) return;
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
// 1. Start recording stream (will prompt screen selection share dialog)
|
|
424
|
+
setStatus("Starting recording...");
|
|
425
|
+
await recorder.startRecording();
|
|
426
|
+
await new Promise((r) => setTimeout(r, 1000)); // wait for video stream to stabilize
|
|
427
|
+
|
|
428
|
+
// 2. Initialize the runner via your backend proxy with custom few-shot instructions/examples
|
|
429
|
+
const { runner, agent } = createRecorderAgentRunner(recorder, {
|
|
430
|
+
baseURL: `${window.location.origin}/api/openai/v1`,
|
|
431
|
+
apiKey: "proxied-by-server", // Server overrides this with the real key
|
|
432
|
+
maxTurns: 20, // Prevents infinite LLM turn loops
|
|
433
|
+
instructions: "Your custom agent instructions and few-shot examples here...",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
setStatus("Agent executing film prompt...");
|
|
437
|
+
|
|
438
|
+
const prompt = "make a 10 second demo that types a product search, submits it, and focuses the result panel";
|
|
439
|
+
const result = await runner.run(agent, prompt);
|
|
440
|
+
setStatus(`Completed: ${result.finalOutput}`);
|
|
441
|
+
} catch (err: any) {
|
|
442
|
+
setStatus(`Error: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div className="agent-tour-card">
|
|
448
|
+
<h3>AI Automated Screen Tour</h3>
|
|
449
|
+
<div className="status">Status: {tourState} {currentScene && `(Scene: ${currentScene})`}</div>
|
|
450
|
+
<button onClick={handleRunTour} disabled={state?.recording}>
|
|
451
|
+
Start Recording Tour
|
|
452
|
+
</button>
|
|
453
|
+
<button onClick={stopTour} disabled={tourState !== "running"}>
|
|
454
|
+
Stop Tour
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
### 2. App-Aware Script Generation
|
|
464
|
+
|
|
465
|
+
Use `generateAppFilmScript()` when an app wants one shared natural-language-to-code.film generator across multiple pages. The SDK owns the generic prompt structure, intent resolution, setup parsing, and setup-aware validation, while your app owns the profile and OpenAI transport.
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import {
|
|
469
|
+
generateAppFilmScript,
|
|
470
|
+
stripAppFilmSetup,
|
|
471
|
+
type AppFilmComplete,
|
|
472
|
+
type AppFilmProfile,
|
|
473
|
+
} from "@codefilm/recorder/app-script";
|
|
474
|
+
|
|
475
|
+
const app: AppFilmProfile = {
|
|
476
|
+
id: "demo-app",
|
|
477
|
+
name: "Demo App",
|
|
478
|
+
selectors: {
|
|
479
|
+
promptBox: {
|
|
480
|
+
selector: ".demo-input-card",
|
|
481
|
+
description: "Prompt composer card",
|
|
482
|
+
role: "input",
|
|
483
|
+
},
|
|
484
|
+
resultPanel: {
|
|
485
|
+
selector: ".demo-output-panel",
|
|
486
|
+
description: "Generated result panel",
|
|
487
|
+
role: "panel",
|
|
488
|
+
requiresSetup: ["result"],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
pages: {
|
|
492
|
+
home: {
|
|
493
|
+
id: "home",
|
|
494
|
+
description: "Main demo page",
|
|
495
|
+
containerSelector: ".demo-app-shell",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
setupCapabilities: ["result", "text"],
|
|
499
|
+
intents: [
|
|
500
|
+
{
|
|
501
|
+
id: "focus-result",
|
|
502
|
+
description: "Show an existing generated result panel.",
|
|
503
|
+
examples: ["result panel", "show generated result"],
|
|
504
|
+
negativeExamples: ["ask the app to generate a new result"],
|
|
505
|
+
page: "home",
|
|
506
|
+
defaultScopeSelector: ".demo-output-panel",
|
|
507
|
+
submitPrompt: false,
|
|
508
|
+
requiredSetup: ["result"],
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: "submit-prompt",
|
|
512
|
+
description: "Type a task into the prompt and submit it.",
|
|
513
|
+
examples: ["make an apple pie website", "use Browser to inspect localhost"],
|
|
514
|
+
page: "home",
|
|
515
|
+
defaultScopeSelector: ".demo-input-card",
|
|
516
|
+
submitPrompt: true,
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
promptTools: [
|
|
520
|
+
{
|
|
521
|
+
id: "browser",
|
|
522
|
+
name: "Browser",
|
|
523
|
+
mention: "@Browser",
|
|
524
|
+
description: "Control the in-app browser.",
|
|
525
|
+
aliases: ["browser", "localhost", "web page"],
|
|
526
|
+
installed: true,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
attachmentTypes: [
|
|
530
|
+
{
|
|
531
|
+
id: "file",
|
|
532
|
+
label: "File",
|
|
533
|
+
aliases: ["file", "attachment"],
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
defaults: {
|
|
537
|
+
minimalPromptMode: true,
|
|
538
|
+
defaultScope: "smallest-meaningful",
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const complete: AppFilmComplete = async (request) => {
|
|
543
|
+
const response = await openai.responses.create({
|
|
544
|
+
model: "gpt-5.5",
|
|
545
|
+
instructions: request.instructions,
|
|
546
|
+
input: request.input,
|
|
547
|
+
tools: request.tools,
|
|
548
|
+
tool_choice: request.toolChoice,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return extractSingleFunctionToolCall(response);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const result = await generateAppFilmScript({
|
|
555
|
+
app,
|
|
556
|
+
input: "focus the generated result panel",
|
|
557
|
+
complete,
|
|
558
|
+
intentMode: "auto",
|
|
559
|
+
repair: true,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
console.log(result.intent?.id); // "focus-result"
|
|
563
|
+
hydrateAppFromFilmSetup(result.setup);
|
|
564
|
+
await recorder.runFilmScript(stripAppFilmSetup(result.script));
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
With profile `intents`, the completion adapter receives a required `resolve_film_intent` tool call followed by `create_film_script`. The resolved intent is passed into script generation and returned as `result.intent`. Set `intentMode: "off"` to skip intent resolution and call `create_film_script` directly.
|
|
568
|
+
|
|
569
|
+
`promptTools` and `attachmentTypes` keep app-native prompt chips structured. For example, a resolved `@Browser` tool or attached `package.json` file appears in `result.intent` and is also serialized as generic setup:
|
|
570
|
+
|
|
571
|
+
```film
|
|
572
|
+
# setup promptTool: {"id":"browser","mention":"@Browser","label":"Browser"}
|
|
573
|
+
# setup attachment: {"type":"file","name":"package.json","label":"package.json"}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
The SDK does not own API keys, proxy URLs, OpenAI clients, host app state, app profiles, selectors, or mock data.
|
|
577
|
+
|
|
578
|
+
Generic setup helpers are available for app hydration:
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
import {
|
|
582
|
+
parseAppFilmSetup,
|
|
583
|
+
prepareAppFilmScript,
|
|
584
|
+
serializeAppFilmSetup,
|
|
585
|
+
stripAppFilmSetup,
|
|
586
|
+
validateAppFilmScript,
|
|
587
|
+
} from "@codefilm/recorder/app-script";
|
|
588
|
+
|
|
589
|
+
const setup = parseAppFilmSetup(result.script, app.id);
|
|
590
|
+
const validation = validateAppFilmScript(result.script, app);
|
|
591
|
+
const executableScript = stripAppFilmSetup(result.script);
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
For route, worker, or storage handoff flows, prefer preparing the script once before displaying or playing it:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
const prepared = prepareAppFilmScript(result.script, {
|
|
598
|
+
app,
|
|
599
|
+
setup: result.setup,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
hydrateAppFromFilmSetup(prepared.setup);
|
|
603
|
+
setEditorText(prepared.displayScript); // setup comments stripped by default
|
|
604
|
+
await recorder.runFilmScript(prepared.runnableScript);
|
|
605
|
+
|
|
606
|
+
const handoffId = crypto.randomUUID();
|
|
607
|
+
sessionStorage.setItem(`host-film:${handoffId}`, prepared.fullScript);
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
On the receiving host surface:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const fullScript = sessionStorage.getItem(`host-film:${handoffId}`) ?? "";
|
|
614
|
+
const prepared = prepareAppFilmScript(fullScript, { app });
|
|
615
|
+
|
|
616
|
+
if (prepared.isFilmScript) {
|
|
617
|
+
hydrateAppFromFilmSetup(prepared.setup);
|
|
618
|
+
setVisibleScript(prepared.displayScript);
|
|
619
|
+
await recorder.runFilmScript(prepared.runnableScript);
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`fullScript` preserves setup comments for debugging and durable handoff. `displayScript` and `runnableScript` strip setup comments by default so metadata such as `# setup collection: ...` is not shown or typed as user prompt text.
|
|
624
|
+
|
|
625
|
+
Enable trace metadata when debugging model/tool behavior:
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { AppFilmGenerationError } from "@codefilm/recorder/app-script";
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const traced = await generateAppFilmScript({
|
|
632
|
+
app,
|
|
633
|
+
input: "use Browser to inspect localhost",
|
|
634
|
+
complete,
|
|
635
|
+
repair: true,
|
|
636
|
+
trace: {
|
|
637
|
+
enabled: true,
|
|
638
|
+
includeRaw: false,
|
|
639
|
+
redact: (call) => ({
|
|
640
|
+
...call,
|
|
641
|
+
request: {
|
|
642
|
+
...call.request,
|
|
643
|
+
input: call.request.input.replace(/sk-[A-Za-z0-9_-]+/g, "[redacted]"),
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
console.log(traced.trace?.calls.map((call) => call.phase));
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (err instanceof AppFilmGenerationError) {
|
|
652
|
+
console.error(err.trace?.calls);
|
|
653
|
+
}
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Trace calls capture `intent`, `script`, and `repair` phases, including tool definitions, tool-call arguments, timings, diagnostics, warnings, and errors. Provider `raw` payloads are omitted unless `includeRaw: true`.
|
|
659
|
+
|
|
660
|
+
#### Prompt-to-Script Examples
|
|
661
|
+
|
|
662
|
+
These examples show how minimal prompts should resolve before script generation. The host app owns the profile recipes, selectors, setup hydration, and prompt-chip rendering; the SDK provides the intent/tool-call structure and validation.
|
|
663
|
+
|
|
664
|
+
##### `edit box`
|
|
665
|
+
|
|
666
|
+
Prompt:
|
|
667
|
+
|
|
668
|
+
```txt
|
|
669
|
+
edit box
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Resolved intent:
|
|
673
|
+
|
|
674
|
+
```json
|
|
675
|
+
{
|
|
676
|
+
"id": "show-edits",
|
|
677
|
+
"page": "thread",
|
|
678
|
+
"scopeSelector": ".preview-card.edited-files",
|
|
679
|
+
"submitPrompt": false,
|
|
680
|
+
"setupNeeded": ["collection", "item", "activeItem", "card:edited_files"],
|
|
681
|
+
"rationale": "The prompt names the edited-files UI, not a task to submit."
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Film script:
|
|
686
|
+
|
|
687
|
+
```film
|
|
688
|
+
# setup collection: {"id":"changes","label":"Changes"}
|
|
689
|
+
# setup item: {"id":"package_edits","collectionId":"changes","label":"Package JSON Edits","time":"now"}
|
|
690
|
+
# setup activeItem: "package_edits"
|
|
691
|
+
# setup card: {"type":"edited_files","itemId":"package_edits","files":[{"path":"package.json","additions":"+1","deletions":"-0"}]}
|
|
692
|
+
|
|
693
|
+
film "Show Package Edits" {
|
|
694
|
+
duration: 8s
|
|
695
|
+
aspect: 16:9
|
|
696
|
+
fps: 30
|
|
697
|
+
|
|
698
|
+
scene "Edited Files" @0s -> 8s {
|
|
699
|
+
camera { selector: ".preview-card.edited-files" padding: 1.16 }
|
|
700
|
+
pointer { selector: ".preview-card.edited-files" durationMs: 600 }
|
|
701
|
+
wait 7s
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
##### `prompt box`
|
|
707
|
+
|
|
708
|
+
Prompt:
|
|
709
|
+
|
|
710
|
+
```txt
|
|
711
|
+
prompt box
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Resolved intent:
|
|
715
|
+
|
|
716
|
+
```json
|
|
717
|
+
{
|
|
718
|
+
"id": "focus-prompt",
|
|
719
|
+
"page": "home",
|
|
720
|
+
"scopeSelector": ".chat-input-card.prompt-box",
|
|
721
|
+
"submitPrompt": false,
|
|
722
|
+
"rationale": "The user named the prompt UI region and did not ask to type or submit."
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Film script:
|
|
727
|
+
|
|
728
|
+
```film
|
|
729
|
+
film "Prompt Box" {
|
|
730
|
+
duration: 5s
|
|
731
|
+
aspect: 16:9
|
|
732
|
+
fps: 30
|
|
733
|
+
|
|
734
|
+
scene "Prompt Box" @0s -> 5s {
|
|
735
|
+
camera { selector: ".chat-input-card.prompt-box" padding: 1.18 }
|
|
736
|
+
pointer { selector: ".chat-input-card.prompt-box" durationMs: 600 }
|
|
737
|
+
wait 4s
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
##### `prompt box with sidebar`
|
|
743
|
+
|
|
744
|
+
Prompt:
|
|
745
|
+
|
|
746
|
+
```txt
|
|
747
|
+
prompt box with sidebar
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Resolved intent:
|
|
751
|
+
|
|
752
|
+
```json
|
|
753
|
+
{
|
|
754
|
+
"id": "focus-prompt",
|
|
755
|
+
"page": "home",
|
|
756
|
+
"scopeSelector": ".app-layout",
|
|
757
|
+
"submitPrompt": false,
|
|
758
|
+
"rationale": "The user asked to include sidebar, so use the full app shell."
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Film script:
|
|
763
|
+
|
|
764
|
+
```film
|
|
765
|
+
film "Prompt Box With Sidebar" {
|
|
766
|
+
duration: 6s
|
|
767
|
+
aspect: 16:9
|
|
768
|
+
fps: 30
|
|
769
|
+
|
|
770
|
+
scene "Prompt And Sidebar" @0s -> 6s {
|
|
771
|
+
camera { selector: ".app-layout" padding: 1.04 }
|
|
772
|
+
pointer { selector: ".chat-input-card.prompt-box" durationMs: 600 }
|
|
773
|
+
wait 5s
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
##### `hello, world`
|
|
779
|
+
|
|
780
|
+
Prompt:
|
|
781
|
+
|
|
782
|
+
```txt
|
|
783
|
+
hello, world
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Resolved intent:
|
|
787
|
+
|
|
788
|
+
```json
|
|
789
|
+
{
|
|
790
|
+
"id": "render-text",
|
|
791
|
+
"page": "text",
|
|
792
|
+
"scopeSelector": ".new-film-text-card",
|
|
793
|
+
"submitPrompt": false,
|
|
794
|
+
"setupNeeded": ["text"],
|
|
795
|
+
"rationale": "The prompt is standalone text to render, not text to submit to the app."
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Film script:
|
|
800
|
+
|
|
801
|
+
```film
|
|
802
|
+
# setup text: {"text":"hello, world","duration":5}
|
|
803
|
+
|
|
804
|
+
film "Hello World" {
|
|
805
|
+
duration: 5s
|
|
806
|
+
aspect: 16:9
|
|
807
|
+
fps: 30
|
|
808
|
+
|
|
809
|
+
scene "Text" @0s -> 5s {
|
|
810
|
+
camera { selector: ".new-film-text-card" padding: 1.18 }
|
|
811
|
+
wait 5s
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
##### `apple pie website`
|
|
817
|
+
|
|
818
|
+
Prompt:
|
|
819
|
+
|
|
820
|
+
```txt
|
|
821
|
+
apple pie website
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
Resolved intent:
|
|
825
|
+
|
|
826
|
+
```json
|
|
827
|
+
{
|
|
828
|
+
"id": "submit-prompt",
|
|
829
|
+
"page": "home",
|
|
830
|
+
"scopeSelector": ".chat-input-card",
|
|
831
|
+
"submitPrompt": true,
|
|
832
|
+
"promptText": "make a website about apple pie"
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Film script:
|
|
837
|
+
|
|
838
|
+
```film
|
|
839
|
+
film "Apple Pie Website" {
|
|
840
|
+
duration: 12s
|
|
841
|
+
aspect: 16:9
|
|
842
|
+
fps: 30
|
|
843
|
+
|
|
844
|
+
scene "Ask Codex" @0s -> 6s {
|
|
845
|
+
camera { selector: ".chat-input-card" padding: 1.16 }
|
|
846
|
+
type { selector: "#codex-prompt-input" text: "make a website about apple pie" cps: 24 }
|
|
847
|
+
pointer { selector: ".send-button" durationMs: 600 }
|
|
848
|
+
wait 800ms
|
|
849
|
+
click { selector: ".send-button" }
|
|
850
|
+
wait 1s
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
scene "Watch Response" @6s -> 12s {
|
|
854
|
+
camera { selector: ".chat-history-container" padding: 1.12 }
|
|
855
|
+
pointer { selector: ".chat-history-container" durationMs: 600 }
|
|
856
|
+
wait 5s
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
##### `type hello into prompt`
|
|
862
|
+
|
|
863
|
+
Prompt:
|
|
864
|
+
|
|
865
|
+
```txt
|
|
866
|
+
type hello into prompt
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Resolved intent:
|
|
870
|
+
|
|
871
|
+
```json
|
|
872
|
+
{
|
|
873
|
+
"id": "submit-prompt",
|
|
874
|
+
"confidence": 0.9,
|
|
875
|
+
"page": "home",
|
|
876
|
+
"scopeSelector": ".chat-input-card",
|
|
877
|
+
"submitPrompt": true,
|
|
878
|
+
"promptText": "hello",
|
|
879
|
+
"rationale": "The user explicitly asked to type text into the prompt."
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Expected script shape:
|
|
884
|
+
|
|
885
|
+
```film
|
|
886
|
+
type { selector: "#codex-prompt-input" text: "hello" cps: 24 }
|
|
887
|
+
pointer { selector: ".send-button" durationMs: 600 }
|
|
888
|
+
wait 800ms
|
|
889
|
+
click { selector: ".send-button" }
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
##### `use Computer to open settings`
|
|
893
|
+
|
|
894
|
+
Prompt:
|
|
895
|
+
|
|
896
|
+
```txt
|
|
897
|
+
use Computer to open settings
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
Resolved intent:
|
|
901
|
+
|
|
902
|
+
```json
|
|
903
|
+
{
|
|
904
|
+
"id": "submit-prompt",
|
|
905
|
+
"page": "home",
|
|
906
|
+
"scopeSelector": ".chat-input-card",
|
|
907
|
+
"submitPrompt": true,
|
|
908
|
+
"promptText": "open System Settings",
|
|
909
|
+
"promptTools": [
|
|
910
|
+
{ "id": "computer_use", "mention": "@Computer", "label": "Computer" }
|
|
911
|
+
]
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Film script:
|
|
916
|
+
|
|
917
|
+
```film
|
|
918
|
+
# setup promptTool: {"id":"computer_use","mention":"@Computer","label":"Computer"}
|
|
919
|
+
|
|
920
|
+
film "Open Settings With Computer" {
|
|
921
|
+
duration: 10s
|
|
922
|
+
aspect: 16:9
|
|
923
|
+
fps: 30
|
|
924
|
+
|
|
925
|
+
scene "Ask Computer" @0s -> 5s {
|
|
926
|
+
camera { selector: ".chat-input-card" padding: 1.16 }
|
|
927
|
+
type { selector: "#codex-prompt-input" text: "@Computer open System Settings" cps: 24 }
|
|
928
|
+
pointer { selector: ".send-button" durationMs: 600 }
|
|
929
|
+
wait 800ms
|
|
930
|
+
click { selector: ".send-button" }
|
|
931
|
+
wait 1s
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
scene "Watch Response" @5s -> 10s {
|
|
935
|
+
camera { selector: ".chat-history-container" padding: 1.12 }
|
|
936
|
+
pointer { selector: ".chat-history-container" durationMs: 600 }
|
|
937
|
+
wait 4s
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
##### Six-word prompts
|
|
943
|
+
|
|
944
|
+
Prompt:
|
|
945
|
+
|
|
946
|
+
```txt
|
|
947
|
+
Show package edits in thread output
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
Film script:
|
|
951
|
+
|
|
952
|
+
```film
|
|
953
|
+
# setup collection: {"id":"changes","label":"Changes"}
|
|
954
|
+
# setup item: {"id":"package_edits","collectionId":"changes","label":"Package JSON Edits","time":"now"}
|
|
955
|
+
# setup activeItem: "package_edits"
|
|
956
|
+
# setup card: {"type":"edited_files","itemId":"package_edits","files":[{"path":"package.json","additions":"+1","deletions":"-0"}]}
|
|
957
|
+
|
|
958
|
+
film "Package Edits" {
|
|
959
|
+
duration: 8s
|
|
960
|
+
aspect: 16:9
|
|
961
|
+
fps: 30
|
|
962
|
+
|
|
963
|
+
scene "Show Edits" @0s -> 8s {
|
|
964
|
+
camera { selector: ".preview-card.edited-files" padding: 1.16 }
|
|
965
|
+
pointer { selector: ".preview-card.edited-files" durationMs: 600 }
|
|
966
|
+
wait 7s
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
Plugin-aware prompts:
|
|
972
|
+
|
|
973
|
+
```txt
|
|
974
|
+
Use Computer to open System Settings
|
|
975
|
+
Ask Browser to inspect localhost homepage
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
Expected script snippets:
|
|
979
|
+
|
|
980
|
+
```film
|
|
981
|
+
# setup promptTool: {"id":"computer_use","mention":"@Computer","label":"Computer"}
|
|
982
|
+
type { selector: "#codex-prompt-input" text: "@Computer open System Settings" cps: 24 }
|
|
983
|
+
|
|
984
|
+
# setup promptTool: {"id":"browser","mention":"@Browser","label":"Browser"}
|
|
985
|
+
type { selector: "#codex-prompt-input" text: "@Browser inspect the localhost homepage" cps: 24 }
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
##### Twelve-word prompts
|
|
989
|
+
|
|
990
|
+
Prompt:
|
|
991
|
+
|
|
992
|
+
```txt
|
|
993
|
+
Show the prompt box with sidebar and active thread visible together
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
Film script:
|
|
997
|
+
|
|
998
|
+
```film
|
|
999
|
+
film "Prompt Box With Sidebar" {
|
|
1000
|
+
duration: 6s
|
|
1001
|
+
aspect: 16:9
|
|
1002
|
+
fps: 30
|
|
1003
|
+
|
|
1004
|
+
scene "Prompt And Sidebar" @0s -> 6s {
|
|
1005
|
+
camera { selector: ".app-layout" padding: 1.04 }
|
|
1006
|
+
pointer { selector: ".chat-input-card.prompt-box" durationMs: 600 }
|
|
1007
|
+
wait 5s
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Plugin-aware prompts:
|
|
1013
|
+
|
|
1014
|
+
```txt
|
|
1015
|
+
Use Documents to summarize attached proposal and show answer in thread output
|
|
1016
|
+
Ask Browser to open localhost then return to prompt box view again
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
Expected script snippets:
|
|
1020
|
+
|
|
1021
|
+
```film
|
|
1022
|
+
# setup promptTool: {"id":"documents","mention":"@Documents","label":"Documents"}
|
|
1023
|
+
# setup attachment: {"type":"file","name":"proposal.docx","label":"proposal.docx"}
|
|
1024
|
+
type { selector: "#codex-prompt-input" text: "@Documents summarize the attached proposal" cps: 24 }
|
|
1025
|
+
|
|
1026
|
+
# setup promptTool: {"id":"browser","mention":"@Browser","label":"Browser"}
|
|
1027
|
+
type { selector: "#codex-prompt-input" text: "@Browser open localhost and report what is visible" cps: 24 }
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
##### Eighteen-word prompts
|
|
1031
|
+
|
|
1032
|
+
Prompt:
|
|
1033
|
+
|
|
1034
|
+
```txt
|
|
1035
|
+
Type apple pie website into Codex, submit it, then show the response appearing in the thread
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Film script:
|
|
1039
|
+
|
|
1040
|
+
```film
|
|
1041
|
+
film "Apple Pie Website" {
|
|
1042
|
+
duration: 12s
|
|
1043
|
+
aspect: 16:9
|
|
1044
|
+
fps: 30
|
|
1045
|
+
|
|
1046
|
+
scene "Submit Prompt" @0s -> 6s {
|
|
1047
|
+
camera { selector: ".chat-input-card" padding: 1.16 }
|
|
1048
|
+
type { selector: "#codex-prompt-input" text: "make a website about apple pie" cps: 24 }
|
|
1049
|
+
pointer { selector: ".send-button" durationMs: 600 }
|
|
1050
|
+
wait 800ms
|
|
1051
|
+
click { selector: ".send-button" }
|
|
1052
|
+
wait 1s
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
scene "Show Response" @6s -> 12s {
|
|
1056
|
+
camera { selector: ".chat-history-container" padding: 1.12 }
|
|
1057
|
+
pointer { selector: ".chat-history-container" durationMs: 600 }
|
|
1058
|
+
wait 5s
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
Plugin-aware prompts:
|
|
1064
|
+
|
|
1065
|
+
```txt
|
|
1066
|
+
Use Computer to open System Settings change nothing then show the prompt and response thread inside Codex UI
|
|
1067
|
+
Attach package.json ask OpenAI Developers to review scripts then show edited files inside prepared thread output view panel
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
Expected script snippets:
|
|
1071
|
+
|
|
1072
|
+
```film
|
|
1073
|
+
# setup promptTool: {"id":"computer_use","mention":"@Computer","label":"Computer"}
|
|
1074
|
+
type { selector: "#codex-prompt-input" text: "@Computer open System Settings and report what is visible without changing anything" cps: 24 }
|
|
1075
|
+
|
|
1076
|
+
# setup promptTool: {"id":"openai_developers","mention":"@OpenAI Developers","label":"OpenAI Developers"}
|
|
1077
|
+
# setup attachment: {"type":"file","name":"package.json","label":"package.json"}
|
|
1078
|
+
type { selector: "#codex-prompt-input" text: "@OpenAI Developers review the package.json scripts" cps: 24 }
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
##### Twenty-four-word prompts
|
|
1082
|
+
|
|
1083
|
+
Prompt:
|
|
1084
|
+
|
|
1085
|
+
```txt
|
|
1086
|
+
Start on a prepared package changes thread, focus the edited files card, then briefly show the thinking and activity rows below
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
Film script:
|
|
1090
|
+
|
|
1091
|
+
```film
|
|
1092
|
+
# setup collection: {"id":"changes","label":"Changes"}
|
|
1093
|
+
# setup item: {"id":"package_edits","collectionId":"changes","label":"Package JSON Edits","time":"now"}
|
|
1094
|
+
# setup activeItem: "package_edits"
|
|
1095
|
+
# setup card: {"type":"edited_files","itemId":"package_edits","files":[{"path":"package.json","additions":"+1","deletions":"-0"}]}
|
|
1096
|
+
|
|
1097
|
+
film "Package Changes Thread" {
|
|
1098
|
+
duration: 12s
|
|
1099
|
+
aspect: 16:9
|
|
1100
|
+
fps: 30
|
|
1101
|
+
|
|
1102
|
+
scene "Edited Files" @0s -> 6s {
|
|
1103
|
+
camera { selector: ".preview-card.edited-files" padding: 1.16 }
|
|
1104
|
+
pointer { selector: ".preview-card.edited-files" durationMs: 600 }
|
|
1105
|
+
wait 5s
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
scene "Thread Activity" @6s -> 12s {
|
|
1109
|
+
camera { selector: ".chat-history-container" padding: 1.12 }
|
|
1110
|
+
pointer { selector: ".thread-activity-status" durationMs: 600 }
|
|
1111
|
+
wait 5s
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
Plugin-aware prompts:
|
|
1117
|
+
|
|
1118
|
+
```txt
|
|
1119
|
+
Use Browser to check localhost attach screenshot ask Build Web Apps to improve hero then show activity and edited files in thread output panel
|
|
1120
|
+
Attach package.json and README ask OpenAI Developers to review release notes then focus response activity row and terminal inside the prepared Codex thread view
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
Expected script snippets:
|
|
1124
|
+
|
|
1125
|
+
```film
|
|
1126
|
+
# setup promptTool: {"id":"browser","mention":"@Browser","label":"Browser"}
|
|
1127
|
+
# setup promptTool: {"id":"build_web_apps","mention":"@Build Web Apps","label":"Build Web Apps"}
|
|
1128
|
+
# setup attachment: {"type":"image","name":"screenshot.png","label":"screenshot.png"}
|
|
1129
|
+
type { selector: "#codex-prompt-input" text: "@Browser @Build Web Apps check localhost and improve the hero using the screenshot" cps: 24 }
|
|
1130
|
+
|
|
1131
|
+
# setup promptTool: {"id":"openai_developers","mention":"@OpenAI Developers","label":"OpenAI Developers"}
|
|
1132
|
+
# setup attachment: {"type":"file","name":"package.json","label":"package.json"}
|
|
1133
|
+
# setup attachment: {"type":"file","name":"README.md","label":"README.md"}
|
|
1134
|
+
type { selector: "#codex-prompt-input" text: "@OpenAI Developers review release notes using package.json and README context" cps: 24 }
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
##### UI state prompts
|
|
1138
|
+
|
|
1139
|
+
Prompt:
|
|
1140
|
+
|
|
1141
|
+
```txt
|
|
1142
|
+
Thinking
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
Film script:
|
|
1146
|
+
|
|
1147
|
+
```film
|
|
1148
|
+
# setup activeItem: "thinking_thread"
|
|
1149
|
+
# setup status: {"type":"thinking","itemId":"thinking_thread","text":"Thinking"}
|
|
1150
|
+
|
|
1151
|
+
film "Thinking Status" {
|
|
1152
|
+
duration: 5s
|
|
1153
|
+
aspect: 16:9
|
|
1154
|
+
fps: 30
|
|
1155
|
+
|
|
1156
|
+
scene "Thinking" @0s -> 5s {
|
|
1157
|
+
camera { selector: ".thread-thinking-status" padding: 1.18 }
|
|
1158
|
+
pointer { selector: ".thread-thinking-status" durationMs: 600 }
|
|
1159
|
+
wait 5s
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
Prompt:
|
|
1165
|
+
|
|
1166
|
+
```txt
|
|
1167
|
+
Worked for 1m 40s, and then go to click Update
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
Film script:
|
|
1171
|
+
|
|
1172
|
+
```film
|
|
1173
|
+
# setup activeItem: "worked_time_thread"
|
|
1174
|
+
# setup status: {"type":"worked_time","itemId":"worked_time_thread","text":"1m 40s"}
|
|
1175
|
+
|
|
1176
|
+
film "Worked Time Then Update" {
|
|
1177
|
+
duration: 9s
|
|
1178
|
+
aspect: 16:9
|
|
1179
|
+
fps: 30
|
|
1180
|
+
|
|
1181
|
+
scene "Worked Time" @0s -> 5s {
|
|
1182
|
+
camera { selector: ".worked-time-bar" padding: 1.18 }
|
|
1183
|
+
pointer { selector: ".worked-time-bar" durationMs: 600 }
|
|
1184
|
+
wait 4s
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
scene "Click Update" @5s -> 9s {
|
|
1188
|
+
camera { selector: ".sidebar-container" padding: 1.12 }
|
|
1189
|
+
pointer { selector: ".update-btn" durationMs: 600 }
|
|
1190
|
+
wait 800ms
|
|
1191
|
+
click { selector: ".update-btn" }
|
|
1192
|
+
wait 2s
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
---
|
|
1198
|
+
|
|
1199
|
+
### 3. Deterministic Structured Script Runner (Offline, Zero-Cost)
|
|
1200
|
+
|
|
1201
|
+
For structured film scripts, you can run them entirely client-side without any LLM API calls. This avoids token usage, latency, and agent turn limits.
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
const script = `
|
|
1205
|
+
film "Product Search Demo" {
|
|
1206
|
+
duration: 10s
|
|
1207
|
+
aspect: 16:9
|
|
1208
|
+
fps: 30
|
|
1209
|
+
|
|
1210
|
+
scene "Prompt" @0s -> 4s {
|
|
1211
|
+
camera { selector: ".demo-input-card" padding: 1.16 }
|
|
1212
|
+
type { selector: "#demo-prompt-input" text: "find ergonomic keyboards" cps: 22 }
|
|
1213
|
+
click { selector: ".demo-submit-button" }
|
|
1214
|
+
wait 900ms
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
scene "Result" @4s -> 7s {
|
|
1218
|
+
camera { selector: ".demo-output-panel" padding: 1.15 }
|
|
1219
|
+
wait 1500ms
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
scene "Details" @7s -> 10s {
|
|
1223
|
+
click { selector: "[data-film-target='details']" }
|
|
1224
|
+
camera { selector: ".demo-details-panel" padding: 1.08 }
|
|
1225
|
+
wait 1600ms
|
|
1226
|
+
camera { selector: null }
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
`;
|
|
1230
|
+
|
|
1231
|
+
// Run offline script directly
|
|
1232
|
+
await recorder.runFilmScript(script, {
|
|
1233
|
+
signal: abortController.signal, // optional AbortSignal to cancel tour execution
|
|
1234
|
+
selectorTimeoutMs: 300, // fail fast if a required selector is not ready
|
|
1235
|
+
typingDefaults: {
|
|
1236
|
+
cps: 24,
|
|
1237
|
+
jitter: 0.22,
|
|
1238
|
+
punctuationPauseMs: 120,
|
|
1239
|
+
mistakeRate: 0.012,
|
|
1240
|
+
},
|
|
1241
|
+
typingSeed: "product-search-demo-v1",
|
|
1242
|
+
onSceneStart: async (scene) => console.log(`Scene started: ${scene.name}`),
|
|
1243
|
+
onSceneEnd: async (scene) => console.log(`Scene ended: ${scene.name}`),
|
|
1244
|
+
waitForCompletion: async ({ signal }) => {
|
|
1245
|
+
await appRenderPromise;
|
|
1246
|
+
if (signal.aborted) return;
|
|
1247
|
+
await waitForReactPaint();
|
|
1248
|
+
},
|
|
1249
|
+
completionTimeoutMs: 120_000,
|
|
1250
|
+
});
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Selector-based actions wait up to `selectorTimeoutMs` for their target. The default is `3000ms` for compatibility, but deterministic recordings often benefit from a shorter run-level timeout after app setup is complete:
|
|
1254
|
+
|
|
1255
|
+
```typescript
|
|
1256
|
+
hydrateAppFromFilmSetup(setup);
|
|
1257
|
+
await waitForRequiredSelectors(script, { timeoutMs: 5000 });
|
|
1258
|
+
await recorder.startRecording();
|
|
1259
|
+
await recorder.runFilmScript(script, {
|
|
1260
|
+
selectorTimeoutMs: 300,
|
|
1261
|
+
});
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
Typing options merge from lowest to highest priority:
|
|
1265
|
+
|
|
1266
|
+
```text
|
|
1267
|
+
SDK defaults → recorder typingDefaults → run typingDefaults → type command fields
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
Use the exported natural preset when one profile is sufficient:
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
import { FilmRecorder, HUMAN_TYPING_PRESET } from "@codefilm/recorder";
|
|
1274
|
+
|
|
1275
|
+
const recorder = new FilmRecorder({
|
|
1276
|
+
typingDefaults: HUMAN_TYPING_PRESET,
|
|
1277
|
+
typingSeed: "repeatable-demo",
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
recorder.setTypingDefaults({
|
|
1281
|
+
cps: 28,
|
|
1282
|
+
jitter: 0.18,
|
|
1283
|
+
});
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
Film scripts support the complete typing vocabulary in one-line or multiline form:
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
type {
|
|
1290
|
+
selector: "#demo-prompt-input"
|
|
1291
|
+
text: "find ergonomic keyboards"
|
|
1292
|
+
cps: 24
|
|
1293
|
+
jitter: 0.22
|
|
1294
|
+
punctuationPauseMs: 120
|
|
1295
|
+
mistakeRate: 0.012
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
The `type` command always performs the full visible interaction sequence internally:
|
|
1300
|
+
|
|
1301
|
+
```text
|
|
1302
|
+
move pointer → click/focus target → human-like type
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
Use `pointer` when the script should visibly focus a target without clicking it:
|
|
1306
|
+
|
|
1307
|
+
```typescript
|
|
1308
|
+
scene "Preview target" @0s -> 2s {
|
|
1309
|
+
camera { selector: ".target" padding: 1.12 }
|
|
1310
|
+
pointer { selector: ".target" durationMs: 600 }
|
|
1311
|
+
wait 500ms
|
|
1312
|
+
}
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
The older `move` command remains supported as a backwards-compatible alias.
|
|
1316
|
+
|
|
1317
|
+
Use `zoom` for full-frame top, center, or bottom camera moves. This exposes the same framing effect as the manual `Cmd/Ctrl + ArrowUp` and `Cmd/Ctrl + ArrowDown` shortcuts:
|
|
1318
|
+
|
|
1319
|
+
```typescript
|
|
1320
|
+
scene "Show lower fold" @0s -> 3s {
|
|
1321
|
+
zoom { to: "bottom" }
|
|
1322
|
+
wait 1s
|
|
1323
|
+
zoom { to: "top" }
|
|
1324
|
+
wait 1s
|
|
1325
|
+
zoom { to: "center" }
|
|
1326
|
+
}
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
Customize the virtual pointer with one or more additional CSS classes:
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
const recorder = new FilmRecorder({
|
|
1333
|
+
pointerClassName: "my-demo-pointer high-contrast",
|
|
1334
|
+
pointerFillColor: "#ffffff",
|
|
1335
|
+
pointerBorderColor: "#18181b",
|
|
1336
|
+
pointerRippleColor: "#64748b",
|
|
1337
|
+
disableRipple: false,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
recorder.setPointerClassName("my-demo-pointer compact");
|
|
1341
|
+
recorder.setPointerFillColor("#f8fafc");
|
|
1342
|
+
recorder.setPointerBorderColor("#0f172a");
|
|
1343
|
+
recorder.setPointerRippleColor("#38bdf8");
|
|
1344
|
+
recorder.setDisableRipple(true);
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
The SDK always preserves its base `cfr-simulated-pointer` class and click-state class.
|
|
1348
|
+
|
|
1349
|
+
Recordings open on the largest top-anchored frame, and the virtual pointer's first movement enters from below the viewport.
|
|
1350
|
+
|
|
1351
|
+
Lifecycle callbacks are async-aware. When a deterministic script or recorder agent finishes successfully, the SDK awaits `onReadyToEnd`, returns the camera to the top frame, holds it for one second, and then stops recording:
|
|
1352
|
+
|
|
1353
|
+
```typescript
|
|
1354
|
+
const recorder = new FilmRecorder({
|
|
1355
|
+
onReadyToEnd: async ({ recorder, signal, scenes }) => {
|
|
1356
|
+
await persistTourState({ signal, sceneCount: scenes.length });
|
|
1357
|
+
console.log("Final closing shot has started", recorder);
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
await recorder.runFilmScript(script, {
|
|
1362
|
+
waitForCompletion: async ({ signal }) => {
|
|
1363
|
+
await appRenderPromise;
|
|
1364
|
+
if (!signal.aborted) await waitForReactPaint();
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
// Recording is stopped after the one-second closing shot.
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
The guaranteed completion order is:
|
|
1371
|
+
|
|
1372
|
+
```text
|
|
1373
|
+
execute scenes
|
|
1374
|
+
→ await waitForCompletion()
|
|
1375
|
+
→ await onReadyToEnd()
|
|
1376
|
+
→ closing camera shot
|
|
1377
|
+
→ stop recording
|
|
1378
|
+
→ create/download artifact
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
For custom integrations, separate script execution from recording finalization:
|
|
1382
|
+
|
|
1383
|
+
```typescript
|
|
1384
|
+
await recorder.executeFilmScript(script, {
|
|
1385
|
+
signal: abortController.signal,
|
|
1386
|
+
});
|
|
1387
|
+
await externalWork;
|
|
1388
|
+
await recorder.finishRecording();
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
`waitForCompletion` observes the unified tour abort signal. It defaults to a 120-second timeout, configurable with `completionTimeoutMs`.
|
|
1392
|
+
|
|
1393
|
+
Register application work from the action that triggered it to avoid stale completion promises:
|
|
1394
|
+
|
|
1395
|
+
```typescript
|
|
1396
|
+
await recorder.runFilmScript(script, {
|
|
1397
|
+
onActionEnd: async ({
|
|
1398
|
+
command,
|
|
1399
|
+
scene,
|
|
1400
|
+
commandIndex,
|
|
1401
|
+
signal,
|
|
1402
|
+
waitUntil,
|
|
1403
|
+
}) => {
|
|
1404
|
+
if (command.type === "click" && command.selector === ".demo-submit-button") {
|
|
1405
|
+
waitUntil(getActiveRenderPromise(signal));
|
|
1406
|
+
}
|
|
1407
|
+
console.log(scene.name, commandIndex);
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
`finishRecording()` is guarded and idempotent. If called while execution or registered gates are active, it waits before starting the closing shot. Use `finishRecording({ force: true })` only to bypass those gates intentionally; `stopAll()` remains the immediate cancellation path.
|
|
1413
|
+
|
|
1414
|
+
Observe `state.executionState` for `idle`, `executing`, `waiting`, `finalizing`, `finished`, and `error`.
|
|
1415
|
+
|
|
1416
|
+
Validate generated scripts before requesting screen capture:
|
|
1417
|
+
|
|
1418
|
+
```typescript
|
|
1419
|
+
const result = recorder.validateFilmScript(script);
|
|
1420
|
+
if (!result.valid) {
|
|
1421
|
+
console.error(result.diagnostics);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
```
|
|
1425
|
+
|
|
1426
|
+
---
|
|
1427
|
+
|
|
1428
|
+
### 3. Human-Like Typing Configuration
|
|
1429
|
+
|
|
1430
|
+
You can make typing simulations look natural (adding variations, punctuation pauses, or mistakes/backspacing) by passing `TypingOptions` to `simulateType`:
|
|
1431
|
+
|
|
1432
|
+
```typescript
|
|
1433
|
+
await recorder.simulateType("#input-field", "Hello world. Let's test the editor!", {
|
|
1434
|
+
cps: 25, // Characters per second
|
|
1435
|
+
jitter: 0.25, // 25% timing jitter variation (adds human speed changes)
|
|
1436
|
+
punctuationPauseMs: 150, // Extra wait after punctuation marks (.,!?;:)
|
|
1437
|
+
mistakeRate: 0.05, // 5% chance of making a typo and backspacing to correct it
|
|
1438
|
+
});
|
|
1439
|
+
```
|
|
1440
|
+
|
|
1441
|
+
---
|
|
1442
|
+
|
|
1443
|
+
### 4. Selector Safety & Text Helpers
|
|
1444
|
+
|
|
1445
|
+
All tool selectors are standard CSS selectors passed to `document.querySelector`. To make writing scripts easier, the SDK provides safe text-based selectors:
|
|
1446
|
+
|
|
1447
|
+
```typescript
|
|
1448
|
+
// Move pointer to and click the button or link containing the text "Plugins"
|
|
1449
|
+
await recorder.clickText("Plugins");
|
|
1450
|
+
|
|
1451
|
+
// Target camera crop and animate pointer to the element containing "Connect files"
|
|
1452
|
+
await recorder.focusText("Connect files");
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
If a selector or text target fails to locate any elements in the DOM, the SDK throws a descriptive, action-oriented error suggesting fixes (like adding stable `data-*` attributes).
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
### 5. Unified Abort/Stop Semantics
|
|
1460
|
+
|
|
1461
|
+
Calling `recorder.stopTour()` or `recorder.stopAll()` will immediately stop all active typing runs, mouse pointer slides, scene waits, and LLM calls by triggering unified inner abort signals.
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
// Aborts active tour runs immediately
|
|
1465
|
+
recorder.stopTour();
|
|
1466
|
+
|
|
1467
|
+
// Aborts active tour runs and stops the browser media stream capture
|
|
1468
|
+
recorder.stopAll();
|
|
1469
|
+
```
|
|
1470
|
+
---
|
|
1471
|
+
|
|
1472
|
+
## Configuration Options (`FilmRecorderOptions`)
|
|
1473
|
+
|
|
1474
|
+
You can customize the recorder behavior by passing options:
|
|
1475
|
+
|
|
1476
|
+
| Property | Type | Default | Description |
|
|
1477
|
+
| :----------------------- | :--------------- | :----------------- | :------------------------------------------------------------------ |
|
|
1478
|
+
| `fps` | `number` | `60` | Capture framerate. |
|
|
1479
|
+
| `resolutions` | `Resolution[]` | _Standard presets_ | Target output aspects (YouTube, TikTok, Instagram, etc.). |
|
|
1480
|
+
| `defaultResolutionIndex` | `number` | `0` | Default active index in the resolutions list. |
|
|
1481
|
+
| `autoZoom` | `boolean` | `true` | Enable focal zooming centered on mouse cursor. |
|
|
1482
|
+
| `pauseDuringScroll` | `boolean` | `true` | Prevent zoom snapping while scrolling page content. |
|
|
1483
|
+
| `showGrid` | `boolean` | `true` | Show helper columns/rows during layout mode. |
|
|
1484
|
+
| `showOutline` | `boolean` | `false` | Include a colored outline around the captured region. |
|
|
1485
|
+
| `enableShortcuts` | `boolean` | `true` | Bind key combinations (`Ctrl+J` / `Cmd+J` for record toggle, etc.). |
|
|
1486
|
+
| `elementSelector` | `string \| null` | `null` | Query selector of the DOM element to track and record. |
|
|
1487
|
+
| `elementPadding` | `number` | `1.1` | Proportional padding multiplier around the tracked element. |
|
|
1488
|
+
| `fixedRecordingRegion` | `"smallest-16:9" \| null` | `null` | Keep the capture box at the smallest 16:9 grid size instead of auto-fitting elements. |
|
|
1489
|
+
| `movePointerToSelector` | `string \| null` | `null` | CSS Selector to automatically run a simulated pointer zoom tour. |
|
|
1490
|
+
| `pointerClassName` | `string` | `""` | Additional CSS class names applied to the virtual pointer element. |
|
|
1491
|
+
| `pointerFillColor` | `string` | `"#ffffff"` | Six-digit hex fill color for the virtual pointer SVG. |
|
|
1492
|
+
| `pointerBorderColor` | `string` | `"#18181b"` | Six-digit hex border color for the virtual pointer SVG. |
|
|
1493
|
+
| `pointerRippleColor` | `string` | `"#64748b"` | Six-digit hex color for the concentric click ripple. |
|
|
1494
|
+
| `disableRipple` | `boolean` | `false` | Disable concentric click ripple while preserving pointer clicks. |
|
|
1495
|
+
| `ignoreSelector` | `string \| null` | `null` | CSS selector for DOM elements to hide during active recording. |
|
|
1496
|
+
| `overlays` | `RecordingOverlayDefinition[]` | `[]` | SDK-managed recording overlays such as `cameraOverlay(...)`. |
|
|
1497
|
+
| `includeTimelineMetadata` | `boolean` | `false` | Attach scene, action, and marker timing metadata to recording artifact. |
|
|
1498
|
+
| `selectorTimeoutMs` | `number` | `3000` | Max wait for selector-based script actions before failing. |
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
## Custom Theming
|
|
1503
|
+
|
|
1504
|
+
Style the overlays and settings bar using CSS variables. Override these variables in your global CSS to match your application's design:
|
|
1505
|
+
|
|
1506
|
+
```css
|
|
1507
|
+
:root {
|
|
1508
|
+
/* Colors */
|
|
1509
|
+
--cfr-bg: rgba(9, 9, 11, 0.9); /* Settings bar background color */
|
|
1510
|
+
--cfr-fg: #f4f4f5; /* Text color */
|
|
1511
|
+
--cfr-border: rgba(255, 255, 255, 0.1); /* Divider / grid line color */
|
|
1512
|
+
--cfr-primary: #f4f4f5; /* Button default background */
|
|
1513
|
+
--cfr-primary-fg: #09090b; /* Button default text */
|
|
1514
|
+
--cfr-secondary: #27272a; /* Secondary button background */
|
|
1515
|
+
--cfr-secondary-fg: #f4f4f5; /* Secondary button text */
|
|
1516
|
+
--cfr-destructive: #ef4444; /* Recording state red background */
|
|
1517
|
+
--cfr-outline: #f43f5e; /* Outline border color when enabled */
|
|
1518
|
+
|
|
1519
|
+
/* Glassmorphism */
|
|
1520
|
+
--cfr-glass-blur: 12px; /* Backdrop blur level */
|
|
1521
|
+
}
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
## Iframe Considerations
|
|
1527
|
+
|
|
1528
|
+
If your application embeds `<iframe>` elements:
|
|
1529
|
+
|
|
1530
|
+
1. **Canvas Cropping is Safe**: The browser's media recorder takes a screen/tab stream approved by the user. Drawing this stream to the canvas **does not taint the canvas**, meaning tab recording works perfectly even with cross-origin frames loaded.
|
|
1531
|
+
2. **Auto Zoom Hover Boundary**: Moving the cursor _inside_ a cross-origin iframe does not bubble mouse events up to the parent React application. To focus areas inside the iframe, use the manual zoom APIs (`zoomTop()`, `zoomCenter()`, `zoomBottom()`), the `zoom { to: "..." }` script command, or the shortcut hotkeys (`Cmd/Ctrl + ArrowDown` or `Cmd/Ctrl + ArrowUp`).
|
|
1532
|
+
|
|
1533
|
+
---
|
|
1534
|
+
|
|
1535
|
+
## Troubleshooting
|
|
1536
|
+
|
|
1537
|
+
### Hook Conflict (Multiple Copies of React)
|
|
1538
|
+
|
|
1539
|
+
When using npm linking for local development, you might encounter a "Hooks can only be called inside the body of a component" error. This is a common issue with local npm packages importing their own React copy.
|
|
1540
|
+
|
|
1541
|
+
**Resolution**: Configure your bundler or build environment to resolve React to the application's root `node_modules`, or create symlinks inside the package folder:
|
|
1542
|
+
|
|
1543
|
+
```bash
|
|
1544
|
+
cd node_modules/@codefilm/recorder
|
|
1545
|
+
npm link ../../node_modules/react
|
|
1546
|
+
npm link ../../node_modules/react-dom
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
---
|
|
1550
|
+
|
|
1551
|
+
## License
|
|
1552
|
+
|
|
1553
|
+
MIT © CodeFilm
|