@helios-project/player 0.48.3

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 ADDED
@@ -0,0 +1,148 @@
1
+ # Helios Player
2
+
3
+ The `@helios-project/player` package provides the `<helios-player>` Web Component, a drop-in UI for reviewing and exporting Helios compositions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @helios-project/player
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Host Page
14
+
15
+ Import the player and use the custom element in your HTML.
16
+
17
+ ```html
18
+ <script type="module">
19
+ import "@helios-project/player";
20
+ </script>
21
+
22
+ <helios-player
23
+ src="path/to/composition.html"
24
+ width="1920"
25
+ height="1080"
26
+ controls
27
+ autoplay
28
+ ></helios-player>
29
+ ```
30
+
31
+ ### Connecting the Composition
32
+
33
+ For the player to control your composition, the composition page (inside the iframe) must connect to the parent window.
34
+
35
+ **If you are using `packages/core` directly:**
36
+
37
+ ```javascript
38
+ import { Helios } from "@helios-project/core";
39
+ import { connectToParent } from "@helios-project/player/bridge";
40
+
41
+ const helios = new Helios({ ... });
42
+
43
+ // Initialize the bridge
44
+ connectToParent(helios);
45
+ ```
46
+
47
+ **If you are using `window.helios` (Legacy/Direct Mode):**
48
+
49
+ The player will automatically attempt to access `window.helios` on the iframe's content window if it's on the same origin. However, `connectToParent` is the recommended approach for cross-origin support and sandboxing.
50
+
51
+ ## Attributes
52
+
53
+ | Attribute | Description | Default |
54
+ |---|---|---|
55
+ | `src` | URL of the composition page to load in the iframe. | (Required) |
56
+ | `width` | Width of the player (aspect ratio calculation). | - |
57
+ | `height` | Height of the player (aspect ratio calculation). | - |
58
+ | `autoplay` | Automatically start playback when connected. | `false` |
59
+ | `loop` | Loop playback when the end is reached. | `false` |
60
+ | `controls` | Show the UI controls overlay. | `false` |
61
+ | `export-mode` | Strategy for client-side export: `auto`, `canvas`, or `dom`. | `auto` |
62
+ | `canvas-selector` | CSS selector for the canvas element (used in `canvas` export mode). | `canvas` |
63
+ | `export-format` | Output video format: `mp4` (H.264/AAC) or `webm` (VP9/Opus). | `mp4` |
64
+ | `poster` | URL of an image to display before playback starts. | - |
65
+ | `preload` | `auto` or `none`. If `none`, defers loading the iframe until interaction. | `auto` |
66
+ | `input-props` | JSON string of properties to pass to the composition. | - |
67
+ | `interactive` | Enable direct interaction with the composition (disables click-to-pause). | `false` |
68
+ | `controlslist` | Space-separated list of features to disable: `nodownload`, `nofullscreen`. | - |
69
+ | `sandbox` | Security flags for the iframe. | `allow-scripts allow-same-origin` |
70
+
71
+ ## CSS Variables
72
+
73
+ The player exposes several CSS variables to allow theming of the controls:
74
+
75
+ | Variable | Default | Description |
76
+ |---|---|---|
77
+ | `--helios-controls-bg` | `rgba(0, 0, 0, 0.6)` | Background color of the controls bar. |
78
+ | `--helios-text-color` | `white` | Text and icon color. |
79
+ | `--helios-accent-color` | `#007bff` | Accent color for active elements (scrubber, buttons). |
80
+ | `--helios-range-track-color` | `#555` | Background color of the scrubber track. |
81
+ | `--helios-font-family` | `sans-serif` | Font family for the player UI. |
82
+
83
+ ## Standard Media API
84
+
85
+ The `<helios-player>` element implements a subset of the HTMLMediaElement interface, allowing you to control playback programmatically.
86
+
87
+ ### Methods
88
+
89
+ - `play(): Promise<void>` - Starts playback.
90
+ - `pause(): void` - Pauses playback.
91
+ - `load(): void` - Reloads the iframe (useful if `src` changed or to retry connection).
92
+ - `addTextTrack(kind: string, label?: string, language?: string): TextTrack` - Adds a new text track to the media element.
93
+
94
+ ### Properties
95
+
96
+ - `textTracks` (TextTrackList, read-only): The text tracks associated with the media element.
97
+ - `currentTime` (number): Current playback position in seconds.
98
+ - `duration` (number, read-only): Total duration in seconds.
99
+ - `paused` (boolean, read-only): Whether playback is paused.
100
+ - `ended` (boolean, read-only): Whether playback has reached the end.
101
+ - `volume` (number): Audio volume (0.0 to 1.0).
102
+ - `muted` (boolean): Audio mute state.
103
+ - `playbackRate` (number): Playback speed (default 1.0).
104
+ - `videoWidth` (number, read-only): The intrinsic width of the video (from controller state or attributes).
105
+ - `videoHeight` (number, read-only): The intrinsic height of the video (from controller state or attributes).
106
+ - `buffered` (TimeRanges, read-only): Returns a TimeRanges object representing buffered content (always 0-duration).
107
+ - `seekable` (TimeRanges, read-only): Returns a TimeRanges object representing seekable content (always 0-duration).
108
+ - `seeking` (boolean, read-only): Whether the player is currently seeking (scrubbing).
109
+ - `readyState` (number, read-only): The current readiness state of the media (0-4).
110
+ - `networkState` (number, read-only): The current network state (0-3).
111
+ - `fps` (number, read-only): Frames per second of the composition.
112
+ - `currentFrame` (number): Current frame index.
113
+ - `inputProps` (object): Get or set the input properties passed to the composition.
114
+
115
+ ## Events
116
+
117
+ The element dispatches the following custom events:
118
+
119
+ - `play`: Fired when playback starts.
120
+ - `pause`: Fired when playback is paused.
121
+ - `ended`: Fired when playback completes.
122
+ - `timeupdate`: Fired when the current time/frame changes.
123
+ - `volumechange`: Fired when volume or mute state changes.
124
+ - `ratechange`: Fired when playback rate changes.
125
+ - `durationchange`: Fired when the duration of the composition changes.
126
+ - `loadstart`: Fired when the browser begins looking for media data.
127
+ - `loadedmetadata`: Fired when the duration and dimensions of the media have been determined.
128
+ - `loadeddata`: Fired when data for the current frame is available.
129
+ - `canplay`: Fired when the browser can resume playback of the media.
130
+ - `canplaythrough`: Fired when the browser estimates it can play through the media without buffering.
131
+
132
+ ## Client-Side Export
133
+
134
+ The player supports exporting the composition to video (MP4/WebM) directly in the browser using WebCodecs.
135
+
136
+ - **`export-mode="canvas"`**: captures frames from a `<canvas>` element. Fast and efficient.
137
+ - **`export-mode="dom"`**: captures the entire DOM using `foreignObject` SVG serialization. Useful for compositions using DOM elements (divs, text, css).
138
+ - **`export-mode="auto"`**: attempts to detect the best strategy.
139
+
140
+ Configuration:
141
+
142
+ ```html
143
+ <helios-player
144
+ src="..."
145
+ export-mode="dom"
146
+ export-format="webm"
147
+ ></helios-player>
148
+ ```
@@ -0,0 +1,2 @@
1
+ import { Helios } from "@helios-project/core";
2
+ export declare function connectToParent(helios: Helios): void;
package/dist/bridge.js ADDED
@@ -0,0 +1,169 @@
1
+ import { captureDomToBitmap } from "./features/dom-capture";
2
+ import { getAudioAssets } from "./features/audio-utils";
3
+ export function connectToParent(helios) {
4
+ // 1. Listen for messages from parent
5
+ window.addEventListener('message', async (event) => {
6
+ if (event.source !== window.parent)
7
+ return;
8
+ const { type, frame } = event.data;
9
+ switch (type) {
10
+ case 'HELIOS_GET_AUDIO_TRACKS':
11
+ const assets = await getAudioAssets(document);
12
+ const buffers = assets.map(a => a.buffer);
13
+ window.parent.postMessage({ type: 'HELIOS_AUDIO_DATA', assets }, '*', buffers);
14
+ break;
15
+ case 'HELIOS_CONNECT':
16
+ // Reply with ready and current state
17
+ window.parent.postMessage({ type: 'HELIOS_READY', state: helios.getState() }, '*');
18
+ window.parent.postMessage({ type: 'HELIOS_STATE', state: helios.getState() }, '*');
19
+ break;
20
+ case 'HELIOS_PLAY':
21
+ helios.play();
22
+ break;
23
+ case 'HELIOS_PAUSE':
24
+ helios.pause();
25
+ break;
26
+ case 'HELIOS_SEEK':
27
+ if (typeof frame === 'number') {
28
+ helios.seek(frame);
29
+ }
30
+ break;
31
+ case 'HELIOS_SET_PLAYBACK_RATE':
32
+ if (typeof event.data.rate === 'number') {
33
+ helios.setPlaybackRate(event.data.rate);
34
+ }
35
+ break;
36
+ case 'HELIOS_SET_PLAYBACK_RANGE':
37
+ const { start, end } = event.data;
38
+ if (typeof start === 'number' && typeof end === 'number') {
39
+ helios.setPlaybackRange(start, end);
40
+ }
41
+ break;
42
+ case 'HELIOS_CLEAR_PLAYBACK_RANGE':
43
+ helios.clearPlaybackRange();
44
+ break;
45
+ case 'HELIOS_SET_VOLUME':
46
+ if (typeof event.data.volume === 'number') {
47
+ helios.setAudioVolume(event.data.volume);
48
+ }
49
+ break;
50
+ case 'HELIOS_SET_MUTED':
51
+ if (typeof event.data.muted === 'boolean') {
52
+ helios.setAudioMuted(event.data.muted);
53
+ }
54
+ break;
55
+ case 'HELIOS_SET_LOOP':
56
+ if (typeof event.data.loop === 'boolean') {
57
+ helios.setLoop(event.data.loop);
58
+ }
59
+ break;
60
+ case 'HELIOS_SET_PROPS':
61
+ if (event.data.props) {
62
+ helios.setInputProps(event.data.props);
63
+ }
64
+ break;
65
+ case 'HELIOS_SET_CAPTIONS':
66
+ if (event.data.captions !== undefined) {
67
+ helios.setCaptions(event.data.captions);
68
+ }
69
+ break;
70
+ case 'HELIOS_GET_SCHEMA':
71
+ window.parent.postMessage({ type: 'HELIOS_SCHEMA', schema: helios.schema }, '*');
72
+ break;
73
+ case 'HELIOS_CAPTURE_FRAME':
74
+ handleCaptureFrame(helios, event.data);
75
+ break;
76
+ }
77
+ });
78
+ // 2. Announce readiness immediately (in case parent is already listening)
79
+ // This helps when the iframe reloads or connects after the parent has already set up listeners
80
+ window.parent.postMessage({ type: 'HELIOS_READY', state: helios.getState() }, '*');
81
+ // 3. Subscribe to Helios state changes and broadcast
82
+ helios.subscribe((state) => {
83
+ window.parent.postMessage({ type: 'HELIOS_STATE', state }, '*');
84
+ });
85
+ // 4. Capture Global Errors
86
+ window.addEventListener('error', (e) => {
87
+ window.parent.postMessage({
88
+ type: 'HELIOS_ERROR',
89
+ error: {
90
+ message: e.message,
91
+ filename: e.filename,
92
+ lineno: e.lineno,
93
+ colno: e.colno,
94
+ stack: e.error?.stack
95
+ }
96
+ }, '*');
97
+ });
98
+ window.addEventListener('unhandledrejection', (e) => {
99
+ window.parent.postMessage({
100
+ type: 'HELIOS_ERROR',
101
+ error: {
102
+ message: e.reason?.message || String(e.reason),
103
+ stack: e.reason?.stack
104
+ }
105
+ }, '*');
106
+ });
107
+ }
108
+ async function handleCaptureFrame(helios, data) {
109
+ const { frame, selector, mode } = data;
110
+ // 1. Seek
111
+ helios.seek(frame);
112
+ // 2. Wait for render (double RAF to be safe and ensure paint)
113
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r())));
114
+ const state = helios.getState();
115
+ const captions = state.activeCaptions || [];
116
+ // 3. DOM Mode
117
+ if (mode === 'dom') {
118
+ try {
119
+ const bitmap = await captureDomToBitmap(document.body);
120
+ window.parent.postMessage({
121
+ type: 'HELIOS_FRAME_DATA',
122
+ frame,
123
+ success: true,
124
+ bitmap,
125
+ captions
126
+ }, '*', [bitmap]);
127
+ }
128
+ catch (e) {
129
+ window.parent.postMessage({
130
+ type: 'HELIOS_FRAME_DATA',
131
+ frame,
132
+ success: false,
133
+ error: e.message
134
+ }, '*');
135
+ }
136
+ return;
137
+ }
138
+ // 4. Canvas Mode (Default)
139
+ const canvas = document.querySelector(selector || 'canvas');
140
+ if (!canvas || !(canvas instanceof HTMLCanvasElement)) {
141
+ window.parent.postMessage({
142
+ type: 'HELIOS_FRAME_DATA',
143
+ frame,
144
+ success: false,
145
+ error: 'Canvas not found'
146
+ }, '*');
147
+ return;
148
+ }
149
+ // 4. Create Bitmap
150
+ try {
151
+ const bitmap = await createImageBitmap(canvas);
152
+ // 5. Send back
153
+ window.parent.postMessage({
154
+ type: 'HELIOS_FRAME_DATA',
155
+ frame,
156
+ success: true,
157
+ bitmap,
158
+ captions
159
+ }, '*', [bitmap]);
160
+ }
161
+ catch (e) {
162
+ window.parent.postMessage({
163
+ type: 'HELIOS_FRAME_DATA',
164
+ frame,
165
+ success: false,
166
+ error: e.message
167
+ }, '*');
168
+ }
169
+ }
@@ -0,0 +1,91 @@
1
+ import { Helios, CaptionCue, HeliosSchema } from "@helios-project/core";
2
+ import { AudioAsset } from "./features/audio-utils";
3
+ export interface HeliosController {
4
+ play(): void;
5
+ pause(): void;
6
+ seek(frame: number): void;
7
+ setAudioVolume(volume: number): void;
8
+ setAudioMuted(muted: boolean): void;
9
+ setLoop(loop: boolean): void;
10
+ setPlaybackRate(rate: number): void;
11
+ setPlaybackRange(startFrame: number, endFrame: number): void;
12
+ clearPlaybackRange(): void;
13
+ setCaptions(captions: string | CaptionCue[]): void;
14
+ setInputProps(props: Record<string, any>): void;
15
+ subscribe(callback: (state: any) => void): () => void;
16
+ onError(callback: (err: any) => void): () => void;
17
+ getState(): any;
18
+ dispose(): void;
19
+ captureFrame(frame: number, options?: {
20
+ selector?: string;
21
+ mode?: 'canvas' | 'dom';
22
+ }): Promise<{
23
+ frame: VideoFrame;
24
+ captions: CaptionCue[];
25
+ } | null>;
26
+ getAudioTracks(): Promise<AudioAsset[]>;
27
+ getSchema(): Promise<HeliosSchema | undefined>;
28
+ }
29
+ export declare class DirectController implements HeliosController {
30
+ instance: Helios;
31
+ private iframe?;
32
+ constructor(instance: Helios, iframe?: HTMLIFrameElement | undefined);
33
+ play(): void;
34
+ pause(): void;
35
+ seek(frame: number): void;
36
+ setAudioVolume(volume: number): void;
37
+ setAudioMuted(muted: boolean): void;
38
+ setLoop(loop: boolean): void;
39
+ setPlaybackRate(rate: number): void;
40
+ setPlaybackRange(start: number, end: number): void;
41
+ clearPlaybackRange(): void;
42
+ setCaptions(captions: string | CaptionCue[]): void;
43
+ setInputProps(props: Record<string, any>): void;
44
+ subscribe(callback: (state: any) => void): () => void;
45
+ getState(): Readonly<import("@helios-project/core").HeliosState>;
46
+ dispose(): void;
47
+ onError(callback: (err: any) => void): () => void;
48
+ getAudioTracks(): Promise<AudioAsset[]>;
49
+ getSchema(): Promise<HeliosSchema | undefined>;
50
+ captureFrame(frame: number, options?: {
51
+ selector?: string;
52
+ mode?: 'canvas' | 'dom';
53
+ }): Promise<{
54
+ frame: VideoFrame;
55
+ captions: CaptionCue[];
56
+ } | null>;
57
+ }
58
+ export declare class BridgeController implements HeliosController {
59
+ private iframeWindow;
60
+ private listeners;
61
+ private errorListeners;
62
+ private lastState;
63
+ constructor(iframeWindow: Window, initialState?: any);
64
+ private handleMessage;
65
+ private notifyListeners;
66
+ private notifyErrorListeners;
67
+ play(): void;
68
+ pause(): void;
69
+ seek(frame: number): void;
70
+ setAudioVolume(volume: number): void;
71
+ setAudioMuted(muted: boolean): void;
72
+ setLoop(loop: boolean): void;
73
+ setPlaybackRate(rate: number): void;
74
+ setPlaybackRange(start: number, end: number): void;
75
+ clearPlaybackRange(): void;
76
+ setCaptions(captions: string | CaptionCue[]): void;
77
+ setInputProps(props: Record<string, any>): void;
78
+ subscribe(callback: (state: any) => void): () => void;
79
+ onError(callback: (err: any) => void): () => void;
80
+ getState(): any;
81
+ dispose(): void;
82
+ captureFrame(frame: number, options?: {
83
+ selector?: string;
84
+ mode?: 'canvas' | 'dom';
85
+ }): Promise<{
86
+ frame: VideoFrame;
87
+ captions: CaptionCue[];
88
+ } | null>;
89
+ getAudioTracks(): Promise<AudioAsset[]>;
90
+ getSchema(): Promise<HeliosSchema | undefined>;
91
+ }
@@ -0,0 +1,224 @@
1
+ import { captureDomToBitmap } from "./features/dom-capture";
2
+ import { getAudioAssets } from "./features/audio-utils";
3
+ export class DirectController {
4
+ instance;
5
+ iframe;
6
+ constructor(instance, iframe) {
7
+ this.instance = instance;
8
+ this.iframe = iframe;
9
+ }
10
+ play() { this.instance.play(); }
11
+ pause() { this.instance.pause(); }
12
+ seek(frame) { this.instance.seek(frame); }
13
+ setAudioVolume(volume) { this.instance.setAudioVolume(volume); }
14
+ setAudioMuted(muted) { this.instance.setAudioMuted(muted); }
15
+ setLoop(loop) { this.instance.setLoop(loop); }
16
+ setPlaybackRate(rate) { this.instance.setPlaybackRate(rate); }
17
+ setPlaybackRange(start, end) { this.instance.setPlaybackRange(start, end); }
18
+ clearPlaybackRange() { this.instance.clearPlaybackRange(); }
19
+ setCaptions(captions) { this.instance.setCaptions(captions); }
20
+ setInputProps(props) { this.instance.setInputProps(props); }
21
+ subscribe(callback) { return this.instance.subscribe(callback); }
22
+ getState() { return this.instance.getState(); }
23
+ dispose() { }
24
+ onError(callback) {
25
+ const target = this.iframe?.contentWindow || window;
26
+ const errorHandler = (event) => {
27
+ callback({
28
+ message: event.message,
29
+ filename: event.filename,
30
+ lineno: event.lineno,
31
+ colno: event.colno,
32
+ stack: event.error?.stack
33
+ });
34
+ };
35
+ const rejectionHandler = (event) => {
36
+ callback({
37
+ message: event.reason?.message || String(event.reason),
38
+ stack: event.reason?.stack
39
+ });
40
+ };
41
+ target.addEventListener('error', errorHandler);
42
+ target.addEventListener('unhandledrejection', rejectionHandler);
43
+ return () => {
44
+ target.removeEventListener('error', errorHandler);
45
+ target.removeEventListener('unhandledrejection', rejectionHandler);
46
+ };
47
+ }
48
+ async getAudioTracks() {
49
+ const doc = this.iframe?.contentDocument || document;
50
+ return getAudioAssets(doc);
51
+ }
52
+ async getSchema() {
53
+ return this.instance.schema;
54
+ }
55
+ async captureFrame(frame, options) {
56
+ const state = this.instance.getState();
57
+ const fps = state.fps;
58
+ const captions = state.activeCaptions || [];
59
+ this.instance.seek(frame);
60
+ // Wait for RAF in iframe to ensure paint
61
+ if (this.iframe && this.iframe.contentWindow) {
62
+ await new Promise(r => this.iframe.contentWindow.requestAnimationFrame(() => r()));
63
+ await new Promise(r => this.iframe.contentWindow.requestAnimationFrame(() => r()));
64
+ }
65
+ const doc = this.iframe?.contentDocument || document;
66
+ let videoFrame = null;
67
+ // Handle DOM mode
68
+ if (options?.mode === 'dom') {
69
+ try {
70
+ const bitmap = await captureDomToBitmap(doc.body);
71
+ videoFrame = new VideoFrame(bitmap, { timestamp: (frame / fps) * 1_000_000 });
72
+ }
73
+ catch (e) {
74
+ console.error("DOM capture failed:", e);
75
+ return null;
76
+ }
77
+ }
78
+ else {
79
+ const selector = options?.selector || 'canvas';
80
+ const canvas = doc.querySelector(selector);
81
+ if (canvas instanceof HTMLCanvasElement) {
82
+ videoFrame = new VideoFrame(canvas, { timestamp: (frame / fps) * 1_000_000 });
83
+ }
84
+ }
85
+ if (videoFrame) {
86
+ return { frame: videoFrame, captions };
87
+ }
88
+ return null;
89
+ }
90
+ }
91
+ export class BridgeController {
92
+ iframeWindow;
93
+ listeners = [];
94
+ errorListeners = [];
95
+ lastState;
96
+ constructor(iframeWindow, initialState) {
97
+ this.iframeWindow = iframeWindow;
98
+ this.lastState = initialState || { isPlaying: false, currentFrame: 0, duration: 0, fps: 30 };
99
+ window.addEventListener('message', this.handleMessage);
100
+ }
101
+ handleMessage = (event) => {
102
+ if (event.source !== this.iframeWindow)
103
+ return;
104
+ if (event.data?.type === 'HELIOS_STATE') {
105
+ this.lastState = event.data.state;
106
+ this.notifyListeners();
107
+ }
108
+ else if (event.data?.type === 'HELIOS_ERROR') {
109
+ this.notifyErrorListeners(event.data.error);
110
+ }
111
+ };
112
+ notifyListeners() {
113
+ this.listeners.forEach(cb => cb(this.lastState));
114
+ }
115
+ notifyErrorListeners(err) {
116
+ this.errorListeners.forEach(cb => cb(err));
117
+ }
118
+ play() { this.iframeWindow.postMessage({ type: 'HELIOS_PLAY' }, '*'); }
119
+ pause() { this.iframeWindow.postMessage({ type: 'HELIOS_PAUSE' }, '*'); }
120
+ seek(frame) { this.iframeWindow.postMessage({ type: 'HELIOS_SEEK', frame }, '*'); }
121
+ setAudioVolume(volume) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_VOLUME', volume }, '*'); }
122
+ setAudioMuted(muted) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_MUTED', muted }, '*'); }
123
+ setLoop(loop) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_LOOP', loop }, '*'); }
124
+ setPlaybackRate(rate) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_PLAYBACK_RATE', rate }, '*'); }
125
+ setPlaybackRange(start, end) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_PLAYBACK_RANGE', start, end }, '*'); }
126
+ clearPlaybackRange() { this.iframeWindow.postMessage({ type: 'HELIOS_CLEAR_PLAYBACK_RANGE' }, '*'); }
127
+ setCaptions(captions) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_CAPTIONS', captions }, '*'); }
128
+ setInputProps(props) { this.iframeWindow.postMessage({ type: 'HELIOS_SET_PROPS', props }, '*'); }
129
+ subscribe(callback) {
130
+ this.listeners.push(callback);
131
+ // Call immediately with current state
132
+ callback(this.lastState);
133
+ return () => {
134
+ this.listeners = this.listeners.filter(l => l !== callback);
135
+ };
136
+ }
137
+ onError(callback) {
138
+ this.errorListeners.push(callback);
139
+ return () => {
140
+ this.errorListeners = this.errorListeners.filter(l => l !== callback);
141
+ };
142
+ }
143
+ getState() { return this.lastState; }
144
+ dispose() {
145
+ window.removeEventListener('message', this.handleMessage);
146
+ this.listeners = [];
147
+ this.errorListeners = [];
148
+ }
149
+ async captureFrame(frame, options) {
150
+ return new Promise((resolve) => {
151
+ const handler = (event) => {
152
+ if (event.source !== this.iframeWindow)
153
+ return;
154
+ if (event.data?.type === 'HELIOS_FRAME_DATA' && event.data.frame === frame) {
155
+ window.removeEventListener('message', handler);
156
+ if (event.data.success) {
157
+ const bitmap = event.data.bitmap;
158
+ const captions = event.data.captions || [];
159
+ // fps from last state
160
+ const fps = this.lastState.fps || 30;
161
+ const videoFrame = new VideoFrame(bitmap, { timestamp: (frame / fps) * 1_000_000 });
162
+ resolve({ frame: videoFrame, captions });
163
+ }
164
+ else {
165
+ console.error("Bridge capture failed:", event.data.error);
166
+ resolve(null);
167
+ }
168
+ }
169
+ };
170
+ window.addEventListener('message', handler);
171
+ this.iframeWindow.postMessage({
172
+ type: 'HELIOS_CAPTURE_FRAME',
173
+ frame,
174
+ selector: options?.selector,
175
+ mode: options?.mode
176
+ }, '*');
177
+ // Timeout
178
+ setTimeout(() => {
179
+ window.removeEventListener('message', handler);
180
+ resolve(null);
181
+ }, 5000);
182
+ });
183
+ }
184
+ async getAudioTracks() {
185
+ return new Promise((resolve) => {
186
+ const handler = (event) => {
187
+ if (event.source !== this.iframeWindow)
188
+ return;
189
+ if (event.data?.type === 'HELIOS_AUDIO_DATA') {
190
+ window.removeEventListener('message', handler);
191
+ resolve(event.data.assets || []);
192
+ }
193
+ };
194
+ window.addEventListener('message', handler);
195
+ this.iframeWindow.postMessage({ type: 'HELIOS_GET_AUDIO_TRACKS' }, '*');
196
+ // Timeout
197
+ setTimeout(() => {
198
+ window.removeEventListener('message', handler);
199
+ resolve([]);
200
+ }, 5000);
201
+ });
202
+ }
203
+ async getSchema() {
204
+ return new Promise((resolve) => {
205
+ let timeoutId;
206
+ const handler = (event) => {
207
+ if (event.source !== this.iframeWindow)
208
+ return;
209
+ if (event.data?.type === 'HELIOS_SCHEMA') {
210
+ window.removeEventListener('message', handler);
211
+ clearTimeout(timeoutId);
212
+ resolve(event.data.schema);
213
+ }
214
+ };
215
+ window.addEventListener('message', handler);
216
+ this.iframeWindow.postMessage({ type: 'HELIOS_GET_SCHEMA' }, '*');
217
+ // Timeout
218
+ timeoutId = window.setTimeout(() => {
219
+ window.removeEventListener('message', handler);
220
+ resolve(undefined);
221
+ }, 5000);
222
+ });
223
+ }
224
+ }
@@ -0,0 +1,10 @@
1
+ export interface AudioAsset {
2
+ buffer: ArrayBuffer;
3
+ mimeType: string | null;
4
+ volume?: number;
5
+ muted?: boolean;
6
+ loop?: boolean;
7
+ startTime?: number;
8
+ }
9
+ export declare function getAudioAssets(doc: Document): Promise<AudioAsset[]>;
10
+ export declare function mixAudio(assets: AudioAsset[], duration: number, sampleRate: number, rangeStart?: number): Promise<AudioBuffer>;