@helios-project/player 0.48.3 → 0.76.5

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 CHANGED
@@ -60,13 +60,56 @@ The player will automatically attempt to access `window.helios` on the iframe's
60
60
  | `controls` | Show the UI controls overlay. | `false` |
61
61
  | `export-mode` | Strategy for client-side export: `auto`, `canvas`, or `dom`. | `auto` |
62
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` |
63
+ | `export-format` | Output format: `mp4`, `webm`, `png`, or `jpeg`. | `mp4` |
64
64
  | `poster` | URL of an image to display before playback starts. | - |
65
65
  | `preload` | `auto` or `none`. If `none`, defers loading the iframe until interaction. | `auto` |
66
66
  | `input-props` | JSON string of properties to pass to the composition. | - |
67
67
  | `interactive` | Enable direct interaction with the composition (disables click-to-pause). | `false` |
68
68
  | `controlslist` | Space-separated list of features to disable: `nodownload`, `nofullscreen`. | - |
69
69
  | `sandbox` | Security flags for the iframe. | `allow-scripts allow-same-origin` |
70
+ | `export-width` | Target width for client-side export. | - |
71
+ | `export-height` | Target height for client-side export. | - |
72
+ | `export-bitrate` | Target bitrate for client-side export (bps). | - |
73
+ | `export-filename` | Filename for client-side export (without extension). | `video` |
74
+ | `export-caption-mode` | Strategy for caption export: `burn-in` or `file`. | `burn-in` |
75
+ | `disablepictureinpicture` | Hides the Picture-in-Picture button. | `false` |
76
+ | `media-title` | Title of the composition for OS Media Session. | - |
77
+ | `media-artist` | Artist name for OS Media Session. | - |
78
+ | `media-album` | Album name for OS Media Session. | - |
79
+ | `media-artwork` | URL of artwork for OS Media Session (defaults to poster). | - |
80
+
81
+ ## User Interface
82
+
83
+ The player includes a comprehensive set of controls:
84
+
85
+ - **Playback**: Play/Pause, Scrubber, Time Display.
86
+ - **Audio**: Volume, Mute, and a Track Menu for individual track control.
87
+ - **Settings Menu** (Gear Icon):
88
+ - **Speed**: Adjust playback rate (0.25x - 2x).
89
+ - **Loop**: Toggle playback looping.
90
+ - **Playback Range**: Set In/Out points to loop a specific section.
91
+ - **Diagnostics**: View environment capabilities (WebCodecs support).
92
+ - **Shortcuts**: View keyboard shortcuts.
93
+ - **Tools**: Fullscreen, Picture-in-Picture, Captions (CC), Export.
94
+
95
+ ### Keyboard Shortcuts
96
+
97
+ | Key | Action |
98
+ |-----|--------|
99
+ | `Space` / `K` | Play / Pause |
100
+ | `F` | Toggle Fullscreen |
101
+ | `M` | Mute / Unmute |
102
+ | `C` | Toggle Captions |
103
+ | `?` | Show Shortcuts Help |
104
+ | `←` / `→` | Seek 1 frame |
105
+ | `Shift` + `←` / `→` | Seek 10 frames |
106
+ | `Home` | Go to Start |
107
+ | `End` | Go to End |
108
+ | `I` | Set In Point |
109
+ | `O` | Set Out Point |
110
+ | `X` | Clear Playback Range |
111
+ | `Shift` + `D` | Toggle Diagnostics |
112
+ | `0-9` | Seek to 0-90% |
70
113
 
71
114
  ## CSS Variables
72
115
 
@@ -90,6 +133,10 @@ The `<helios-player>` element implements a subset of the HTMLMediaElement interf
90
133
  - `pause(): void` - Pauses playback.
91
134
  - `load(): void` - Reloads the iframe (useful if `src` changed or to retry connection).
92
135
  - `addTextTrack(kind: string, label?: string, language?: string): TextTrack` - Adds a new text track to the media element.
136
+ - `diagnose(): Promise<DiagnosticReport>` - Runs environment diagnostics (WebCodecs, WebGL) and returns a report.
137
+ - `requestPictureInPicture(): Promise<PictureInPictureWindow>` - Requests Picture-in-Picture mode for the player.
138
+ - `export(options?: HeliosExportOptions): Promise<void>` - Programmatically trigger client-side export.
139
+ - `fastSeek(time: number): void` - Seeks to the specified time as fast as possible (currently equivalent to setting `currentTime`).
93
140
 
94
141
  ### Properties
95
142
 
@@ -101,6 +148,8 @@ The `<helios-player>` element implements a subset of the HTMLMediaElement interf
101
148
  - `volume` (number): Audio volume (0.0 to 1.0).
102
149
  - `muted` (boolean): Audio mute state.
103
150
  - `playbackRate` (number): Playback speed (default 1.0).
151
+ - `width` (number): Reflected width attribute.
152
+ - `height` (number): Reflected height attribute.
104
153
  - `videoWidth` (number, read-only): The intrinsic width of the video (from controller state or attributes).
105
154
  - `videoHeight` (number, read-only): The intrinsic height of the video (from controller state or attributes).
106
155
  - `buffered` (TimeRanges, read-only): Returns a TimeRanges object representing buffered content (always 0-duration).
@@ -111,6 +160,8 @@ The `<helios-player>` element implements a subset of the HTMLMediaElement interf
111
160
  - `fps` (number, read-only): Frames per second of the composition.
112
161
  - `currentFrame` (number): Current frame index.
113
162
  - `inputProps` (object): Get or set the input properties passed to the composition.
163
+ - `playsInline` (boolean): Reflected playsinline attribute.
164
+ - `disablePictureInPicture` (boolean): Hides the Picture-in-Picture button.
114
165
 
115
166
  ## Events
116
167
 
@@ -131,7 +182,7 @@ The element dispatches the following custom events:
131
182
 
132
183
  ## Client-Side Export
133
184
 
134
- The player supports exporting the composition to video (MP4/WebM) directly in the browser using WebCodecs.
185
+ The player supports exporting the composition to video (MP4/WebM) or image snapshots (PNG/JPEG) directly in the browser using WebCodecs.
135
186
 
136
187
  - **`export-mode="canvas"`**: captures frames from a `<canvas>` element. Fast and efficient.
137
188
  - **`export-mode="dom"`**: captures the entire DOM using `foreignObject` SVG serialization. Useful for compositions using DOM elements (divs, text, css).
@@ -146,3 +197,23 @@ Configuration:
146
197
  export-format="webm"
147
198
  ></helios-player>
148
199
  ```
200
+
201
+ To take a snapshot (PNG) instead of a video, set `export-format="png"`.
202
+
203
+ ### Audio Fades
204
+
205
+ To apply audio fades during client-side export, add `data-helios-fade-in` and/or `data-helios-fade-out` attributes to your audio elements within the composition. The value should be the duration in seconds.
206
+
207
+ ```html
208
+ <audio src="music.mp3" data-helios-fade-in="2" data-helios-fade-out="3"></audio>
209
+ ```
210
+
211
+ ## Verification
212
+
213
+ To run the End-to-End (E2E) verification suite:
214
+
215
+ ```bash
216
+ npx tsx tests/e2e/verify-player.ts
217
+ ```
218
+
219
+ This script starts a local server and uses Playwright to verify the player's core functionality (playback, scrubber, menus, volume) using a dependency-free mock composition.
package/dist/bridge.js CHANGED
@@ -1,6 +1,14 @@
1
+ import { Helios } from "@helios-project/core";
1
2
  import { captureDomToBitmap } from "./features/dom-capture";
2
3
  import { getAudioAssets } from "./features/audio-utils";
4
+ import { AudioMeter } from "./features/audio-metering";
5
+ import { AudioFader } from "./features/audio-fader";
3
6
  export function connectToParent(helios) {
7
+ let audioMeter = null;
8
+ let audioMeterRaf = null;
9
+ const audioFader = new AudioFader();
10
+ // Connect fader immediately (scans document for audio elements)
11
+ audioFader.connect(document);
4
12
  // 1. Listen for messages from parent
5
13
  window.addEventListener('message', async (event) => {
6
14
  if (event.source !== window.parent)
@@ -8,7 +16,8 @@ export function connectToParent(helios) {
8
16
  const { type, frame } = event.data;
9
17
  switch (type) {
10
18
  case 'HELIOS_GET_AUDIO_TRACKS':
11
- const assets = await getAudioAssets(document);
19
+ const state = helios.getState();
20
+ const assets = await getAudioAssets(document, state.availableAudioTracks, state.audioTracks);
12
21
  const buffers = assets.map(a => a.buffer);
13
22
  window.parent.postMessage({ type: 'HELIOS_AUDIO_DATA', assets }, '*', buffers);
14
23
  break;
@@ -26,6 +35,11 @@ export function connectToParent(helios) {
26
35
  case 'HELIOS_SEEK':
27
36
  if (typeof frame === 'number') {
28
37
  helios.seek(frame);
38
+ requestAnimationFrame(() => {
39
+ requestAnimationFrame(() => {
40
+ window.parent.postMessage({ type: 'HELIOS_SEEK_DONE', frame }, '*');
41
+ });
42
+ });
29
43
  }
30
44
  break;
31
45
  case 'HELIOS_SET_PLAYBACK_RATE':
@@ -52,6 +66,16 @@ export function connectToParent(helios) {
52
66
  helios.setAudioMuted(event.data.muted);
53
67
  }
54
68
  break;
69
+ case 'HELIOS_SET_AUDIO_TRACK_VOLUME':
70
+ if (typeof event.data.trackId === 'string' && typeof event.data.volume === 'number') {
71
+ helios.setAudioTrackVolume(event.data.trackId, event.data.volume);
72
+ }
73
+ break;
74
+ case 'HELIOS_SET_AUDIO_TRACK_MUTED':
75
+ if (typeof event.data.trackId === 'string' && typeof event.data.muted === 'boolean') {
76
+ helios.setAudioTrackMuted(event.data.trackId, event.data.muted);
77
+ }
78
+ break;
55
79
  case 'HELIOS_SET_LOOP':
56
80
  if (typeof event.data.loop === 'boolean') {
57
81
  helios.setLoop(event.data.loop);
@@ -67,9 +91,59 @@ export function connectToParent(helios) {
67
91
  helios.setCaptions(event.data.captions);
68
92
  }
69
93
  break;
94
+ case 'HELIOS_SET_DURATION':
95
+ if (typeof event.data.duration === 'number') {
96
+ helios.setDuration(event.data.duration);
97
+ }
98
+ break;
99
+ case 'HELIOS_SET_FPS':
100
+ if (typeof event.data.fps === 'number') {
101
+ helios.setFps(event.data.fps);
102
+ }
103
+ break;
104
+ case 'HELIOS_SET_SIZE':
105
+ if (typeof event.data.width === 'number' && typeof event.data.height === 'number') {
106
+ helios.setSize(event.data.width, event.data.height);
107
+ }
108
+ break;
109
+ case 'HELIOS_SET_MARKERS':
110
+ if (Array.isArray(event.data.markers)) {
111
+ helios.setMarkers(event.data.markers);
112
+ }
113
+ break;
70
114
  case 'HELIOS_GET_SCHEMA':
71
115
  window.parent.postMessage({ type: 'HELIOS_SCHEMA', schema: helios.schema }, '*');
72
116
  break;
117
+ case 'HELIOS_START_METERING':
118
+ if (!audioMeter) {
119
+ audioMeter = new AudioMeter();
120
+ }
121
+ audioMeter.connect(document);
122
+ audioMeter.enable();
123
+ if (!audioMeterRaf) {
124
+ const loop = () => {
125
+ if (audioMeter) {
126
+ const levels = audioMeter.getLevels();
127
+ window.parent.postMessage({ type: 'HELIOS_AUDIO_LEVELS', levels }, '*');
128
+ }
129
+ audioMeterRaf = requestAnimationFrame(loop);
130
+ };
131
+ audioMeterRaf = requestAnimationFrame(loop);
132
+ }
133
+ break;
134
+ case 'HELIOS_STOP_METERING':
135
+ if (audioMeterRaf) {
136
+ cancelAnimationFrame(audioMeterRaf);
137
+ audioMeterRaf = null;
138
+ }
139
+ if (audioMeter) {
140
+ audioMeter.disable();
141
+ }
142
+ break;
143
+ case 'HELIOS_DIAGNOSE':
144
+ const report = await Helios.diagnose();
145
+ window.parent.postMessage({ type: 'HELIOS_DIAGNOSE_RESULT', report }, '*');
146
+ break;
73
147
  case 'HELIOS_CAPTURE_FRAME':
74
148
  handleCaptureFrame(helios, event.data);
75
149
  break;
@@ -80,6 +154,12 @@ export function connectToParent(helios) {
80
154
  window.parent.postMessage({ type: 'HELIOS_READY', state: helios.getState() }, '*');
81
155
  // 3. Subscribe to Helios state changes and broadcast
82
156
  helios.subscribe((state) => {
157
+ if (state.isPlaying) {
158
+ audioFader.enable();
159
+ }
160
+ else {
161
+ audioFader.disable();
162
+ }
83
163
  window.parent.postMessage({ type: 'HELIOS_STATE', state }, '*');
84
164
  });
85
165
  // 4. Capture Global Errors
@@ -106,7 +186,7 @@ export function connectToParent(helios) {
106
186
  });
107
187
  }
108
188
  async function handleCaptureFrame(helios, data) {
109
- const { frame, selector, mode } = data;
189
+ const { frame, selector, mode, width, height } = data;
110
190
  // 1. Seek
111
191
  helios.seek(frame);
112
192
  // 2. Wait for render (double RAF to be safe and ensure paint)
@@ -116,7 +196,7 @@ async function handleCaptureFrame(helios, data) {
116
196
  // 3. DOM Mode
117
197
  if (mode === 'dom') {
118
198
  try {
119
- const bitmap = await captureDomToBitmap(document.body);
199
+ const bitmap = await captureDomToBitmap(document.body, { targetWidth: width, targetHeight: height });
120
200
  window.parent.postMessage({
121
201
  type: 'HELIOS_FRAME_DATA',
122
202
  frame,
@@ -148,7 +228,28 @@ async function handleCaptureFrame(helios, data) {
148
228
  }
149
229
  // 4. Create Bitmap
150
230
  try {
151
- const bitmap = await createImageBitmap(canvas);
231
+ let source = canvas;
232
+ if (width && height && (canvas.width !== width || canvas.height !== height)) {
233
+ if (typeof OffscreenCanvas !== 'undefined') {
234
+ const offscreen = new OffscreenCanvas(width, height);
235
+ const ctx = offscreen.getContext('2d');
236
+ if (ctx) {
237
+ ctx.drawImage(canvas, 0, 0, width, height);
238
+ source = offscreen;
239
+ }
240
+ }
241
+ else {
242
+ const tempCanvas = document.createElement('canvas');
243
+ tempCanvas.width = width;
244
+ tempCanvas.height = height;
245
+ const ctx = tempCanvas.getContext('2d');
246
+ if (ctx) {
247
+ ctx.drawImage(canvas, 0, 0, width, height);
248
+ source = tempCanvas;
249
+ }
250
+ }
251
+ }
252
+ const bitmap = await createImageBitmap(source);
152
253
  // 5. Send back
153
254
  window.parent.postMessage({
154
255
  type: 'HELIOS_FRAME_DATA',
@@ -1,17 +1,24 @@
1
- import { Helios, CaptionCue, HeliosSchema } from "@helios-project/core";
1
+ import type { Helios, CaptionCue, HeliosSchema, DiagnosticReport, Marker } from "@helios-project/core";
2
2
  import { AudioAsset } from "./features/audio-utils";
3
+ import { AudioLevels } from "./features/audio-metering";
3
4
  export interface HeliosController {
4
5
  play(): void;
5
6
  pause(): void;
6
- seek(frame: number): void;
7
+ seek(frame: number): Promise<void>;
7
8
  setAudioVolume(volume: number): void;
8
9
  setAudioMuted(muted: boolean): void;
10
+ setAudioTrackVolume(trackId: string, volume: number): void;
11
+ setAudioTrackMuted(trackId: string, muted: boolean): void;
9
12
  setLoop(loop: boolean): void;
10
13
  setPlaybackRate(rate: number): void;
11
14
  setPlaybackRange(startFrame: number, endFrame: number): void;
12
15
  clearPlaybackRange(): void;
13
16
  setCaptions(captions: string | CaptionCue[]): void;
14
17
  setInputProps(props: Record<string, any>): void;
18
+ setDuration(seconds: number): void;
19
+ setFps(fps: number): void;
20
+ setSize(width: number, height: number): void;
21
+ setMarkers(markers: Marker[]): void;
15
22
  subscribe(callback: (state: any) => void): () => void;
16
23
  onError(callback: (err: any) => void): () => void;
17
24
  getState(): any;
@@ -19,73 +26,109 @@ export interface HeliosController {
19
26
  captureFrame(frame: number, options?: {
20
27
  selector?: string;
21
28
  mode?: 'canvas' | 'dom';
29
+ width?: number;
30
+ height?: number;
22
31
  }): Promise<{
23
32
  frame: VideoFrame;
24
33
  captions: CaptionCue[];
25
34
  } | null>;
26
35
  getAudioTracks(): Promise<AudioAsset[]>;
27
36
  getSchema(): Promise<HeliosSchema | undefined>;
37
+ diagnose(): Promise<DiagnosticReport>;
38
+ startAudioMetering(): void;
39
+ stopAudioMetering(): void;
40
+ onAudioMetering(callback: (levels: AudioLevels) => void): () => void;
28
41
  }
29
42
  export declare class DirectController implements HeliosController {
30
43
  instance: Helios;
31
44
  private iframe?;
45
+ private audioMeter;
46
+ private audioFader;
47
+ private audioMeteringCallback;
48
+ private audioMeteringRaf;
32
49
  constructor(instance: Helios, iframe?: HTMLIFrameElement | undefined);
33
50
  play(): void;
34
51
  pause(): void;
35
- seek(frame: number): void;
52
+ seek(frame: number): Promise<void>;
36
53
  setAudioVolume(volume: number): void;
37
54
  setAudioMuted(muted: boolean): void;
55
+ setAudioTrackVolume(trackId: string, volume: number): void;
56
+ setAudioTrackMuted(trackId: string, muted: boolean): void;
38
57
  setLoop(loop: boolean): void;
39
58
  setPlaybackRate(rate: number): void;
40
59
  setPlaybackRange(start: number, end: number): void;
41
60
  clearPlaybackRange(): void;
42
61
  setCaptions(captions: string | CaptionCue[]): void;
43
62
  setInputProps(props: Record<string, any>): void;
63
+ setDuration(seconds: number): void;
64
+ setFps(fps: number): void;
65
+ setSize(width: number, height: number): void;
66
+ setMarkers(markers: Marker[]): void;
44
67
  subscribe(callback: (state: any) => void): () => void;
45
- getState(): Readonly<import("@helios-project/core").HeliosState>;
68
+ getState(): Readonly<import("@helios-project/core").HeliosState<Record<string, any>>>;
46
69
  dispose(): void;
70
+ startAudioMetering(): void;
71
+ stopAudioMetering(): void;
72
+ onAudioMetering(callback: (levels: AudioLevels) => void): () => void;
47
73
  onError(callback: (err: any) => void): () => void;
48
74
  getAudioTracks(): Promise<AudioAsset[]>;
49
75
  getSchema(): Promise<HeliosSchema | undefined>;
50
76
  captureFrame(frame: number, options?: {
51
77
  selector?: string;
52
78
  mode?: 'canvas' | 'dom';
79
+ width?: number;
80
+ height?: number;
53
81
  }): Promise<{
54
82
  frame: VideoFrame;
55
83
  captions: CaptionCue[];
56
84
  } | null>;
85
+ diagnose(): Promise<DiagnosticReport>;
57
86
  }
58
87
  export declare class BridgeController implements HeliosController {
59
88
  private iframeWindow;
60
89
  private listeners;
61
90
  private errorListeners;
91
+ private audioMeteringListeners;
62
92
  private lastState;
63
93
  constructor(iframeWindow: Window, initialState?: any);
64
94
  private handleMessage;
65
95
  private notifyListeners;
66
96
  private notifyErrorListeners;
97
+ private notifyAudioMeteringListeners;
67
98
  play(): void;
68
99
  pause(): void;
69
- seek(frame: number): void;
100
+ seek(frame: number): Promise<void>;
70
101
  setAudioVolume(volume: number): void;
71
102
  setAudioMuted(muted: boolean): void;
103
+ setAudioTrackVolume(trackId: string, volume: number): void;
104
+ setAudioTrackMuted(trackId: string, muted: boolean): void;
72
105
  setLoop(loop: boolean): void;
73
106
  setPlaybackRate(rate: number): void;
74
107
  setPlaybackRange(start: number, end: number): void;
75
108
  clearPlaybackRange(): void;
76
109
  setCaptions(captions: string | CaptionCue[]): void;
77
110
  setInputProps(props: Record<string, any>): void;
111
+ setDuration(duration: number): void;
112
+ setFps(fps: number): void;
113
+ setSize(width: number, height: number): void;
114
+ setMarkers(markers: Marker[]): void;
115
+ startAudioMetering(): void;
116
+ stopAudioMetering(): void;
78
117
  subscribe(callback: (state: any) => void): () => void;
79
118
  onError(callback: (err: any) => void): () => void;
119
+ onAudioMetering(callback: (levels: AudioLevels) => void): () => void;
80
120
  getState(): any;
81
121
  dispose(): void;
82
122
  captureFrame(frame: number, options?: {
83
123
  selector?: string;
84
124
  mode?: 'canvas' | 'dom';
125
+ width?: number;
126
+ height?: number;
85
127
  }): Promise<{
86
128
  frame: VideoFrame;
87
129
  captions: CaptionCue[];
88
130
  } | null>;
89
131
  getAudioTracks(): Promise<AudioAsset[]>;
90
132
  getSchema(): Promise<HeliosSchema | undefined>;
133
+ diagnose(): Promise<DiagnosticReport>;
91
134
  }