@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 +148 -0
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +169 -0
- package/dist/controllers.d.ts +91 -0
- package/dist/controllers.js +224 -0
- package/dist/features/audio-utils.d.ts +10 -0
- package/dist/features/audio-utils.js +66 -0
- package/dist/features/dom-capture.d.ts +1 -0
- package/dist/features/dom-capture.js +253 -0
- package/dist/features/exporter.d.ts +18 -0
- package/dist/features/exporter.js +228 -0
- package/dist/features/srt-parser.d.ts +7 -0
- package/dist/features/srt-parser.js +75 -0
- package/dist/features/text-tracks.d.ts +40 -0
- package/dist/features/text-tracks.js +99 -0
- package/dist/helios-player.bundle.mjs +7775 -0
- package/dist/helios-player.global.js +633 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +1679 -0
- package/package.json +57 -0
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
|
+
```
|
package/dist/bridge.d.ts
ADDED
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>;
|