@waveform-playlist/media-element-playout 12.0.0 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/index.d.mts +98 -4
- package/dist/index.d.ts +98 -4
- package/dist/index.js +167 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +167 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ playout.play(0); // Play from beginning
|
|
|
56
56
|
playout.setPlaybackRate(0.75); // Slow down to 75% speed (pitch preserved)
|
|
57
57
|
playout.pause();
|
|
58
58
|
playout.seekTo(30); // Seek to 30 seconds
|
|
59
|
-
playout.
|
|
59
|
+
playout.resume(); // Resume from the current position (does NOT reset to 0)
|
|
60
60
|
|
|
61
61
|
// Clean up
|
|
62
62
|
playout.dispose();
|
|
@@ -79,16 +79,22 @@ class MediaElementPlayout {
|
|
|
79
79
|
|
|
80
80
|
// Track management
|
|
81
81
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
82
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack; // silent in-place replace
|
|
82
83
|
removeTrack(trackId: string): void;
|
|
83
84
|
getTrack(trackId: string): MediaElementTrack | undefined;
|
|
84
85
|
|
|
85
86
|
// Playback
|
|
86
87
|
play(when?: number, offset?: number, duration?: number): void;
|
|
88
|
+
resume(): void; // resume from current position (no reset to 0)
|
|
87
89
|
pause(): void;
|
|
88
90
|
stop(): void;
|
|
89
91
|
seekTo(time: number): void;
|
|
90
92
|
getCurrentTime(): number;
|
|
91
93
|
|
|
94
|
+
// Lifecycle events (typed via MediaElementTrackEvents)
|
|
95
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
96
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
97
|
+
|
|
92
98
|
// Volume & Rate
|
|
93
99
|
setMasterVolume(volume: number): void;
|
|
94
100
|
setPlaybackRate(rate: number): void; // 0.5 to 2.0, pitch preserved
|
|
@@ -105,7 +111,7 @@ class MediaElementPlayout {
|
|
|
105
111
|
```typescript
|
|
106
112
|
interface MediaElementTrackOptions {
|
|
107
113
|
source: string | HTMLAudioElement; // URL or audio element
|
|
108
|
-
peaks
|
|
114
|
+
peaks?: WaveformDataObject; // Pre-computed peaks (optional — omit for scrubber-only / headless players)
|
|
109
115
|
id?: string;
|
|
110
116
|
name?: string;
|
|
111
117
|
volume?: number;
|
|
@@ -113,6 +119,31 @@ interface MediaElementTrackOptions {
|
|
|
113
119
|
}
|
|
114
120
|
```
|
|
115
121
|
|
|
122
|
+
## Player Mode
|
|
123
|
+
|
|
124
|
+
Beyond the timeline/editor API, three affordances make this engine pleasant to
|
|
125
|
+
reuse as a single-track **player** (podcast/audiobook players, `<daw-player>`):
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Resume from the current position (play() with no offset resets to 0)
|
|
129
|
+
playout.resume();
|
|
130
|
+
|
|
131
|
+
// Swap to the next source in place — no "Only one track is supported" warning,
|
|
132
|
+
// and any Web Audio routing/effects are preserved across the swap
|
|
133
|
+
playout.setSource({ source: '/audio/episode-2.mp3', name: 'Episode 2' });
|
|
134
|
+
|
|
135
|
+
// Observe media lifecycle without reaching into the audio element
|
|
136
|
+
playout.on('loadedmetadata', () => console.log('duration:', playout.duration));
|
|
137
|
+
playout.on('play', () => updateTransportUI('playing'));
|
|
138
|
+
playout.on('pause', () => updateTransportUI('paused'));
|
|
139
|
+
playout.on('error', (err) => surfaceError(err));
|
|
140
|
+
playout.off('play', handler); // unsubscribe
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`on()` listeners are retained across `setSource()` swaps — register them once.
|
|
144
|
+
The same `on()/off()` and `resume()`/`load()` methods exist on `MediaElementTrack`
|
|
145
|
+
for power users. Event names and payloads are typed via `MediaElementTrackEvents`.
|
|
146
|
+
|
|
116
147
|
## Generating Peaks
|
|
117
148
|
|
|
118
149
|
Use [audiowaveform](https://github.com/bbc/audiowaveform) or [waveform-data.js](https://github.com/bbc/waveform-data.js) to pre-compute peaks:
|
package/dist/index.d.mts
CHANGED
|
@@ -4,8 +4,9 @@ export { FadeConfig } from '@waveform-playlist/core';
|
|
|
4
4
|
interface MediaElementTrackOptions {
|
|
5
5
|
/** The audio source - can be a URL, Blob URL, or HTMLAudioElement */
|
|
6
6
|
source: string | HTMLAudioElement;
|
|
7
|
-
/** Pre-computed waveform data for visualization
|
|
8
|
-
|
|
7
|
+
/** Pre-computed waveform data for visualization. Optional — omit for
|
|
8
|
+
* scrubber-only / headless players that don't render a waveform. */
|
|
9
|
+
peaks?: WaveformDataObject;
|
|
9
10
|
/** Track ID */
|
|
10
11
|
id?: string;
|
|
11
12
|
/** Track name for display */
|
|
@@ -33,6 +34,25 @@ interface MediaElementTrackOptions {
|
|
|
33
34
|
/** Fade out configuration (requires audioContext) */
|
|
34
35
|
fadeOut?: FadeConfig;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()
|
|
39
|
+
* pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and
|
|
40
|
+
* the dawcore web-components layer can wire events uniformly across engines.
|
|
41
|
+
*/
|
|
42
|
+
interface MediaElementTrackEvents {
|
|
43
|
+
/** Fired when the element's metadata (duration, dimensions) has loaded. */
|
|
44
|
+
loadedmetadata: () => void;
|
|
45
|
+
/** Fired when native playback starts/resumes. */
|
|
46
|
+
play: () => void;
|
|
47
|
+
/** Fired when native playback pauses (including at end-of-media). */
|
|
48
|
+
pause: () => void;
|
|
49
|
+
/** Fired on a media error; carries the element's MediaError (or null). */
|
|
50
|
+
error: (err: MediaError | null) => void;
|
|
51
|
+
/** Fired when playback reaches the end of the media. */
|
|
52
|
+
ended: () => void;
|
|
53
|
+
/** Fired on each native timeupdate; carries the current time in seconds. */
|
|
54
|
+
timeupdate: (time: number) => void;
|
|
55
|
+
}
|
|
36
56
|
/**
|
|
37
57
|
* Single-track playback using HTMLAudioElement.
|
|
38
58
|
*
|
|
@@ -56,6 +76,7 @@ declare class MediaElementTrack {
|
|
|
56
76
|
private _volume;
|
|
57
77
|
private onStopCallback?;
|
|
58
78
|
private onTimeUpdateCallback?;
|
|
79
|
+
private _listeners;
|
|
59
80
|
private _audioContext;
|
|
60
81
|
private _sourceNode;
|
|
61
82
|
private _fadeGain;
|
|
@@ -65,6 +86,10 @@ declare class MediaElementTrack {
|
|
|
65
86
|
constructor(options: MediaElementTrackOptions);
|
|
66
87
|
private handleEnded;
|
|
67
88
|
private handleTimeUpdate;
|
|
89
|
+
private handleLoadedMetadata;
|
|
90
|
+
private handlePlay;
|
|
91
|
+
private handlePause;
|
|
92
|
+
private handleError;
|
|
68
93
|
/**
|
|
69
94
|
* Schedule fade automation on the fade GainNode.
|
|
70
95
|
* Called at the start of each play() — fades are relative to the playback offset.
|
|
@@ -80,6 +105,29 @@ declare class MediaElementTrack {
|
|
|
80
105
|
* (fades depend on audioContext.currentTime being non-zero).
|
|
81
106
|
*/
|
|
82
107
|
play(offset?: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Resume playback from the current position without resetting currentTime.
|
|
110
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
111
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
112
|
+
* playback where it was.
|
|
113
|
+
*/
|
|
114
|
+
resume(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
117
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
118
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
119
|
+
*
|
|
120
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
121
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
122
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
123
|
+
*
|
|
124
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
125
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
126
|
+
*/
|
|
127
|
+
load(source: string, opts?: {
|
|
128
|
+
peaks?: WaveformDataObject;
|
|
129
|
+
name?: string;
|
|
130
|
+
}): void;
|
|
83
131
|
/**
|
|
84
132
|
* Pause playback
|
|
85
133
|
*/
|
|
@@ -131,13 +179,23 @@ declare class MediaElementTrack {
|
|
|
131
179
|
* Disconnect the output and reconnect to the default AudioContext destination.
|
|
132
180
|
*/
|
|
133
181
|
disconnectOutput(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
184
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
185
|
+
*/
|
|
186
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
187
|
+
/**
|
|
188
|
+
* Unsubscribe a previously registered listener.
|
|
189
|
+
*/
|
|
190
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
191
|
+
private _emit;
|
|
134
192
|
/**
|
|
135
193
|
* Clean up resources
|
|
136
194
|
*/
|
|
137
195
|
dispose(): void;
|
|
138
196
|
get id(): string;
|
|
139
197
|
get name(): string;
|
|
140
|
-
get peaks(): WaveformDataObject;
|
|
198
|
+
get peaks(): WaveformDataObject | null;
|
|
141
199
|
get currentTime(): number;
|
|
142
200
|
get duration(): number;
|
|
143
201
|
get isPlaying(): boolean;
|
|
@@ -188,6 +246,8 @@ declare class MediaElementPlayout {
|
|
|
188
246
|
private _preservesPitch;
|
|
189
247
|
private _isPlaying;
|
|
190
248
|
private onPlaybackCompleteCallback?;
|
|
249
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
250
|
+
private _eventListeners;
|
|
191
251
|
constructor(options?: MediaElementPlayoutOptions);
|
|
192
252
|
/**
|
|
193
253
|
* Initialize the playout engine.
|
|
@@ -201,6 +261,16 @@ declare class MediaElementPlayout {
|
|
|
201
261
|
* Note: Only one track is supported. Adding a second track will dispose the first.
|
|
202
262
|
*/
|
|
203
263
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
264
|
+
/**
|
|
265
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
266
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
267
|
+
*
|
|
268
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
269
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
270
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
271
|
+
* the track. Returns the active track.
|
|
272
|
+
*/
|
|
273
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack;
|
|
204
274
|
/**
|
|
205
275
|
* Remove a track by ID.
|
|
206
276
|
*/
|
|
@@ -216,6 +286,13 @@ declare class MediaElementPlayout {
|
|
|
216
286
|
* @param duration - Duration to play in seconds (optional)
|
|
217
287
|
*/
|
|
218
288
|
play(_when?: number, offset?: number, duration?: number): void;
|
|
289
|
+
/**
|
|
290
|
+
* Resume playback from the current position (player-mode affordance).
|
|
291
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
292
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
293
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
294
|
+
*/
|
|
295
|
+
resume(): void;
|
|
219
296
|
/**
|
|
220
297
|
* Pause playback.
|
|
221
298
|
*/
|
|
@@ -253,6 +330,23 @@ declare class MediaElementPlayout {
|
|
|
253
330
|
* Set callback for when playback completes.
|
|
254
331
|
*/
|
|
255
332
|
setOnPlaybackComplete(callback: () => void): void;
|
|
333
|
+
/**
|
|
334
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
335
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
336
|
+
* retained and re-attached automatically when the source is swapped.
|
|
337
|
+
*/
|
|
338
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
339
|
+
/**
|
|
340
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
341
|
+
*/
|
|
342
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
343
|
+
/**
|
|
344
|
+
* Attach every registered listener to the current track. Called after a new
|
|
345
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
346
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
347
|
+
* the registry; TS cannot track it through this loop.
|
|
348
|
+
*/
|
|
349
|
+
private _attachListenersToTrack;
|
|
256
350
|
/**
|
|
257
351
|
* Clean up resources.
|
|
258
352
|
*/
|
|
@@ -306,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
|
|
|
306
400
|
*/
|
|
307
401
|
declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
|
|
308
402
|
|
|
309
|
-
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
|
403
|
+
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackEvents, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export { FadeConfig } from '@waveform-playlist/core';
|
|
|
4
4
|
interface MediaElementTrackOptions {
|
|
5
5
|
/** The audio source - can be a URL, Blob URL, or HTMLAudioElement */
|
|
6
6
|
source: string | HTMLAudioElement;
|
|
7
|
-
/** Pre-computed waveform data for visualization
|
|
8
|
-
|
|
7
|
+
/** Pre-computed waveform data for visualization. Optional — omit for
|
|
8
|
+
* scrubber-only / headless players that don't render a waveform. */
|
|
9
|
+
peaks?: WaveformDataObject;
|
|
9
10
|
/** Track ID */
|
|
10
11
|
id?: string;
|
|
11
12
|
/** Track name for display */
|
|
@@ -33,6 +34,25 @@ interface MediaElementTrackOptions {
|
|
|
33
34
|
/** Fade out configuration (requires audioContext) */
|
|
34
35
|
fadeOut?: FadeConfig;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Typed event map for MediaElementTrack's emitter. Mirrors the on()/off()
|
|
39
|
+
* pattern used by @waveform-playlist/engine's PlaylistEngine so consumers and
|
|
40
|
+
* the dawcore web-components layer can wire events uniformly across engines.
|
|
41
|
+
*/
|
|
42
|
+
interface MediaElementTrackEvents {
|
|
43
|
+
/** Fired when the element's metadata (duration, dimensions) has loaded. */
|
|
44
|
+
loadedmetadata: () => void;
|
|
45
|
+
/** Fired when native playback starts/resumes. */
|
|
46
|
+
play: () => void;
|
|
47
|
+
/** Fired when native playback pauses (including at end-of-media). */
|
|
48
|
+
pause: () => void;
|
|
49
|
+
/** Fired on a media error; carries the element's MediaError (or null). */
|
|
50
|
+
error: (err: MediaError | null) => void;
|
|
51
|
+
/** Fired when playback reaches the end of the media. */
|
|
52
|
+
ended: () => void;
|
|
53
|
+
/** Fired on each native timeupdate; carries the current time in seconds. */
|
|
54
|
+
timeupdate: (time: number) => void;
|
|
55
|
+
}
|
|
36
56
|
/**
|
|
37
57
|
* Single-track playback using HTMLAudioElement.
|
|
38
58
|
*
|
|
@@ -56,6 +76,7 @@ declare class MediaElementTrack {
|
|
|
56
76
|
private _volume;
|
|
57
77
|
private onStopCallback?;
|
|
58
78
|
private onTimeUpdateCallback?;
|
|
79
|
+
private _listeners;
|
|
59
80
|
private _audioContext;
|
|
60
81
|
private _sourceNode;
|
|
61
82
|
private _fadeGain;
|
|
@@ -65,6 +86,10 @@ declare class MediaElementTrack {
|
|
|
65
86
|
constructor(options: MediaElementTrackOptions);
|
|
66
87
|
private handleEnded;
|
|
67
88
|
private handleTimeUpdate;
|
|
89
|
+
private handleLoadedMetadata;
|
|
90
|
+
private handlePlay;
|
|
91
|
+
private handlePause;
|
|
92
|
+
private handleError;
|
|
68
93
|
/**
|
|
69
94
|
* Schedule fade automation on the fade GainNode.
|
|
70
95
|
* Called at the start of each play() — fades are relative to the playback offset.
|
|
@@ -80,6 +105,29 @@ declare class MediaElementTrack {
|
|
|
80
105
|
* (fades depend on audioContext.currentTime being non-zero).
|
|
81
106
|
*/
|
|
82
107
|
play(offset?: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Resume playback from the current position without resetting currentTime.
|
|
110
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
111
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
112
|
+
* playback where it was.
|
|
113
|
+
*/
|
|
114
|
+
resume(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
117
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
118
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
119
|
+
*
|
|
120
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
121
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
122
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
123
|
+
*
|
|
124
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
125
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
126
|
+
*/
|
|
127
|
+
load(source: string, opts?: {
|
|
128
|
+
peaks?: WaveformDataObject;
|
|
129
|
+
name?: string;
|
|
130
|
+
}): void;
|
|
83
131
|
/**
|
|
84
132
|
* Pause playback
|
|
85
133
|
*/
|
|
@@ -131,13 +179,23 @@ declare class MediaElementTrack {
|
|
|
131
179
|
* Disconnect the output and reconnect to the default AudioContext destination.
|
|
132
180
|
*/
|
|
133
181
|
disconnectOutput(): void;
|
|
182
|
+
/**
|
|
183
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
184
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
185
|
+
*/
|
|
186
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
187
|
+
/**
|
|
188
|
+
* Unsubscribe a previously registered listener.
|
|
189
|
+
*/
|
|
190
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
191
|
+
private _emit;
|
|
134
192
|
/**
|
|
135
193
|
* Clean up resources
|
|
136
194
|
*/
|
|
137
195
|
dispose(): void;
|
|
138
196
|
get id(): string;
|
|
139
197
|
get name(): string;
|
|
140
|
-
get peaks(): WaveformDataObject;
|
|
198
|
+
get peaks(): WaveformDataObject | null;
|
|
141
199
|
get currentTime(): number;
|
|
142
200
|
get duration(): number;
|
|
143
201
|
get isPlaying(): boolean;
|
|
@@ -188,6 +246,8 @@ declare class MediaElementPlayout {
|
|
|
188
246
|
private _preservesPitch;
|
|
189
247
|
private _isPlaying;
|
|
190
248
|
private onPlaybackCompleteCallback?;
|
|
249
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
250
|
+
private _eventListeners;
|
|
191
251
|
constructor(options?: MediaElementPlayoutOptions);
|
|
192
252
|
/**
|
|
193
253
|
* Initialize the playout engine.
|
|
@@ -201,6 +261,16 @@ declare class MediaElementPlayout {
|
|
|
201
261
|
* Note: Only one track is supported. Adding a second track will dispose the first.
|
|
202
262
|
*/
|
|
203
263
|
addTrack(options: MediaElementTrackOptions): MediaElementTrack;
|
|
264
|
+
/**
|
|
265
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
266
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
267
|
+
*
|
|
268
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
269
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
270
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
271
|
+
* the track. Returns the active track.
|
|
272
|
+
*/
|
|
273
|
+
setSource(options: MediaElementTrackOptions): MediaElementTrack;
|
|
204
274
|
/**
|
|
205
275
|
* Remove a track by ID.
|
|
206
276
|
*/
|
|
@@ -216,6 +286,13 @@ declare class MediaElementPlayout {
|
|
|
216
286
|
* @param duration - Duration to play in seconds (optional)
|
|
217
287
|
*/
|
|
218
288
|
play(_when?: number, offset?: number, duration?: number): void;
|
|
289
|
+
/**
|
|
290
|
+
* Resume playback from the current position (player-mode affordance).
|
|
291
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
292
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
293
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
294
|
+
*/
|
|
295
|
+
resume(): void;
|
|
219
296
|
/**
|
|
220
297
|
* Pause playback.
|
|
221
298
|
*/
|
|
@@ -253,6 +330,23 @@ declare class MediaElementPlayout {
|
|
|
253
330
|
* Set callback for when playback completes.
|
|
254
331
|
*/
|
|
255
332
|
setOnPlaybackComplete(callback: () => void): void;
|
|
333
|
+
/**
|
|
334
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
335
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
336
|
+
* retained and re-attached automatically when the source is swapped.
|
|
337
|
+
*/
|
|
338
|
+
on<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
339
|
+
/**
|
|
340
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
341
|
+
*/
|
|
342
|
+
off<K extends keyof MediaElementTrackEvents>(event: K, listener: MediaElementTrackEvents[K]): void;
|
|
343
|
+
/**
|
|
344
|
+
* Attach every registered listener to the current track. Called after a new
|
|
345
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
346
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
347
|
+
* the registry; TS cannot track it through this loop.
|
|
348
|
+
*/
|
|
349
|
+
private _attachListenersToTrack;
|
|
256
350
|
/**
|
|
257
351
|
* Clean up resources.
|
|
258
352
|
*/
|
|
@@ -306,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
|
|
|
306
400
|
*/
|
|
307
401
|
declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
|
|
308
402
|
|
|
309
|
-
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
|
403
|
+
export { MediaElementPlayout, type MediaElementPlayoutOptions, MediaElementTrack, type MediaElementTrackEvents, type MediaElementTrackOptions, type PlaybackRateEngine, type PlayoutEngine, supportsPlaybackRate };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var import_core = require("@waveform-playlist/core");
|
|
|
31
31
|
var MediaElementTrack = class {
|
|
32
32
|
constructor(options) {
|
|
33
33
|
this._playbackRate = 1;
|
|
34
|
+
this._listeners = /* @__PURE__ */ new Map();
|
|
34
35
|
// Web Audio nodes (only when audioContext is provided)
|
|
35
36
|
this._audioContext = null;
|
|
36
37
|
this._sourceNode = null;
|
|
@@ -41,13 +42,27 @@ var MediaElementTrack = class {
|
|
|
41
42
|
if (this.onStopCallback) {
|
|
42
43
|
this.onStopCallback();
|
|
43
44
|
}
|
|
45
|
+
this._emit("ended");
|
|
44
46
|
};
|
|
45
47
|
this.handleTimeUpdate = () => {
|
|
46
48
|
if (this.onTimeUpdateCallback) {
|
|
47
49
|
this.onTimeUpdateCallback(this.audioElement.currentTime);
|
|
48
50
|
}
|
|
51
|
+
this._emit("timeupdate", this.audioElement.currentTime);
|
|
49
52
|
};
|
|
50
|
-
this.
|
|
53
|
+
this.handleLoadedMetadata = () => {
|
|
54
|
+
this._emit("loadedmetadata");
|
|
55
|
+
};
|
|
56
|
+
this.handlePlay = () => {
|
|
57
|
+
this._emit("play");
|
|
58
|
+
};
|
|
59
|
+
this.handlePause = () => {
|
|
60
|
+
this._emit("pause");
|
|
61
|
+
};
|
|
62
|
+
this.handleError = () => {
|
|
63
|
+
this._emit("error", this.audioElement.error);
|
|
64
|
+
};
|
|
65
|
+
this._peaks = options.peaks ?? null;
|
|
51
66
|
this._id = options.id ?? `track-${Date.now()}`;
|
|
52
67
|
this._name = options.name ?? "Track";
|
|
53
68
|
this._playbackRate = options.playbackRate ?? 1;
|
|
@@ -93,6 +108,10 @@ var MediaElementTrack = class {
|
|
|
93
108
|
}
|
|
94
109
|
this.audioElement.addEventListener("ended", this.handleEnded);
|
|
95
110
|
this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
|
|
111
|
+
this.audioElement.addEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
112
|
+
this.audioElement.addEventListener("play", this.handlePlay);
|
|
113
|
+
this.audioElement.addEventListener("pause", this.handlePause);
|
|
114
|
+
this.audioElement.addEventListener("error", this.handleError);
|
|
96
115
|
}
|
|
97
116
|
/**
|
|
98
117
|
* Schedule fade automation on the fade GainNode.
|
|
@@ -179,6 +198,45 @@ var MediaElementTrack = class {
|
|
|
179
198
|
startPlayback();
|
|
180
199
|
}
|
|
181
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Resume playback from the current position without resetting currentTime.
|
|
203
|
+
* Reuses play()'s fade re-scheduling and AudioContext-resume machinery —
|
|
204
|
+
* passing the current position as the offset is a no-op seek that leaves
|
|
205
|
+
* playback where it was.
|
|
206
|
+
*/
|
|
207
|
+
resume() {
|
|
208
|
+
this.play(this.currentTime);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Swap the audio source in place, reusing the existing <audio> element.
|
|
212
|
+
* Because the MediaElementAudioSourceNode is once-per-element, reusing the
|
|
213
|
+
* element preserves any Web Audio routing/effects across the swap.
|
|
214
|
+
*
|
|
215
|
+
* Only supported when this track owns its element (constructed from a URL
|
|
216
|
+
* string). A borrowed element (constructed from an HTMLAudioElement) warns
|
|
217
|
+
* and no-ops — swapping a consumer-owned element's src is out of contract.
|
|
218
|
+
*
|
|
219
|
+
* Peaks are coupled to the specific audio, so they are replaced (defaulting
|
|
220
|
+
* to null when omitted). Name is a label, so it updates only when provided.
|
|
221
|
+
*/
|
|
222
|
+
load(source, opts = {}) {
|
|
223
|
+
if (!this.ownsElement) {
|
|
224
|
+
console.warn(
|
|
225
|
+
"[waveform-playlist] MediaElementTrack: load() is only supported for tracks that own their audio element (constructed from a URL string). A track constructed from an existing HTMLAudioElement cannot swap its source in place."
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this._cancelFades();
|
|
230
|
+
this.audioElement.pause();
|
|
231
|
+
this.audioElement.src = source;
|
|
232
|
+
this.audioElement.load();
|
|
233
|
+
this.audioElement.currentTime = 0;
|
|
234
|
+
this._peaks = opts.peaks ?? null;
|
|
235
|
+
if (opts.name !== void 0) {
|
|
236
|
+
this._name = opts.name;
|
|
237
|
+
}
|
|
238
|
+
this.audioElement.playbackRate = this._playbackRate;
|
|
239
|
+
}
|
|
182
240
|
/**
|
|
183
241
|
* Pause playback
|
|
184
242
|
*/
|
|
@@ -285,12 +343,47 @@ var MediaElementTrack = class {
|
|
|
285
343
|
}
|
|
286
344
|
this._volumeGain.connect(this._audioContext.destination);
|
|
287
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Subscribe to a track lifecycle event. Multiple listeners per event are
|
|
348
|
+
* supported. Mirrors PlaylistEngine's on()/off() emitter.
|
|
349
|
+
*/
|
|
350
|
+
on(event, listener) {
|
|
351
|
+
if (!this._listeners.has(event)) {
|
|
352
|
+
this._listeners.set(event, /* @__PURE__ */ new Set());
|
|
353
|
+
}
|
|
354
|
+
this._listeners.get(event).add(listener);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Unsubscribe a previously registered listener.
|
|
358
|
+
*/
|
|
359
|
+
off(event, listener) {
|
|
360
|
+
this._listeners.get(event)?.delete(listener);
|
|
361
|
+
}
|
|
362
|
+
_emit(event, ...args) {
|
|
363
|
+
const listeners = this._listeners.get(event);
|
|
364
|
+
if (listeners) {
|
|
365
|
+
for (const listener of listeners) {
|
|
366
|
+
try {
|
|
367
|
+
listener(...args);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.warn(
|
|
370
|
+
"[waveform-playlist] MediaElementTrack: error in event listener: " + String(error)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
288
376
|
/**
|
|
289
377
|
* Clean up resources
|
|
290
378
|
*/
|
|
291
379
|
dispose() {
|
|
292
380
|
this.audioElement.removeEventListener("ended", this.handleEnded);
|
|
293
381
|
this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
|
|
382
|
+
this.audioElement.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
|
383
|
+
this.audioElement.removeEventListener("play", this.handlePlay);
|
|
384
|
+
this.audioElement.removeEventListener("pause", this.handlePause);
|
|
385
|
+
this.audioElement.removeEventListener("error", this.handleError);
|
|
386
|
+
this._listeners.clear();
|
|
294
387
|
this._cancelFades();
|
|
295
388
|
this.audioElement.pause();
|
|
296
389
|
if (this._sourceNode) {
|
|
@@ -339,7 +432,7 @@ var MediaElementTrack = class {
|
|
|
339
432
|
return this.audioElement.currentTime;
|
|
340
433
|
}
|
|
341
434
|
get duration() {
|
|
342
|
-
return this.audioElement.duration || this._peaks
|
|
435
|
+
return this.audioElement.duration || this._peaks?.duration || 0;
|
|
343
436
|
}
|
|
344
437
|
get isPlaying() {
|
|
345
438
|
return !this.audioElement.paused && !this.audioElement.ended;
|
|
@@ -373,6 +466,8 @@ var MediaElementPlayout = class {
|
|
|
373
466
|
constructor(options = {}) {
|
|
374
467
|
this.track = null;
|
|
375
468
|
this._isPlaying = false;
|
|
469
|
+
/** Consumer event listeners, retained so they re-attach across track swaps. */
|
|
470
|
+
this._eventListeners = /* @__PURE__ */ new Map();
|
|
376
471
|
this._masterVolume = options.masterVolume ?? 1;
|
|
377
472
|
this._playbackRate = options.playbackRate ?? 1;
|
|
378
473
|
this._preservesPitch = options.preservesPitch ?? true;
|
|
@@ -408,8 +503,30 @@ var MediaElementPlayout = class {
|
|
|
408
503
|
this.onPlaybackCompleteCallback();
|
|
409
504
|
}
|
|
410
505
|
});
|
|
506
|
+
this._attachListenersToTrack();
|
|
411
507
|
return this.track;
|
|
412
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Replace the playout's source (player-mode affordance). The documented
|
|
511
|
+
* single-track replace path — does NOT warn like addTrack().
|
|
512
|
+
*
|
|
513
|
+
* For URL (string) sources with an existing track, swaps in place via
|
|
514
|
+
* track.load(), reusing the element and preserving Web Audio routing. For a
|
|
515
|
+
* provided HTMLAudioElement source, or when there is no track yet, (re)creates
|
|
516
|
+
* the track. Returns the active track.
|
|
517
|
+
*/
|
|
518
|
+
setSource(options) {
|
|
519
|
+
this._isPlaying = false;
|
|
520
|
+
if (this.track && typeof options.source === "string") {
|
|
521
|
+
this.track.load(options.source, { peaks: options.peaks, name: options.name });
|
|
522
|
+
return this.track;
|
|
523
|
+
}
|
|
524
|
+
if (this.track) {
|
|
525
|
+
this.track.dispose();
|
|
526
|
+
this.track = null;
|
|
527
|
+
}
|
|
528
|
+
return this.addTrack(options);
|
|
529
|
+
}
|
|
413
530
|
/**
|
|
414
531
|
* Remove a track by ID.
|
|
415
532
|
*/
|
|
@@ -454,6 +571,15 @@ var MediaElementPlayout = class {
|
|
|
454
571
|
}, adjustedDuration * 1e3);
|
|
455
572
|
}
|
|
456
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Resume playback from the current position (player-mode affordance).
|
|
576
|
+
* Unlike play() with no offset (which resets to 0), this keeps currentTime.
|
|
577
|
+
* Delegates to play() with the current position as the offset, so all of
|
|
578
|
+
* play()'s machinery (AudioContext resume, fades, _isPlaying) is reused.
|
|
579
|
+
*/
|
|
580
|
+
resume() {
|
|
581
|
+
this.play(void 0, this.getCurrentTime());
|
|
582
|
+
}
|
|
457
583
|
/**
|
|
458
584
|
* Pause playback.
|
|
459
585
|
*/
|
|
@@ -529,6 +655,43 @@ var MediaElementPlayout = class {
|
|
|
529
655
|
setOnPlaybackComplete(callback) {
|
|
530
656
|
this.onPlaybackCompleteCallback = callback;
|
|
531
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Subscribe to a lifecycle event (loadedmetadata / play / pause / error /
|
|
660
|
+
* ended / timeupdate) without reaching into track.element. Listeners are
|
|
661
|
+
* retained and re-attached automatically when the source is swapped.
|
|
662
|
+
*/
|
|
663
|
+
on(event, listener) {
|
|
664
|
+
if (!this._eventListeners.has(event)) {
|
|
665
|
+
this._eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
666
|
+
}
|
|
667
|
+
this._eventListeners.get(event).add(listener);
|
|
668
|
+
this.track?.on(event, listener);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Unsubscribe a previously registered lifecycle listener.
|
|
672
|
+
*/
|
|
673
|
+
off(event, listener) {
|
|
674
|
+
this._eventListeners.get(event)?.delete(listener);
|
|
675
|
+
this.track?.off(event, listener);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Attach every registered listener to the current track. Called after a new
|
|
679
|
+
* track is created so subscriptions survive source swaps. The cast is safe:
|
|
680
|
+
* the event→listener correlation was enforced by the typed on() that filled
|
|
681
|
+
* the registry; TS cannot track it through this loop.
|
|
682
|
+
*/
|
|
683
|
+
_attachListenersToTrack() {
|
|
684
|
+
const track = this.track;
|
|
685
|
+
if (!track) return;
|
|
686
|
+
for (const [event, listeners] of this._eventListeners) {
|
|
687
|
+
for (const listener of listeners) {
|
|
688
|
+
track.on(
|
|
689
|
+
event,
|
|
690
|
+
listener
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
532
695
|
/**
|
|
533
696
|
* Clean up resources.
|
|
534
697
|
*/
|
|
@@ -537,6 +700,7 @@ var MediaElementPlayout = class {
|
|
|
537
700
|
this.track.dispose();
|
|
538
701
|
this.track = null;
|
|
539
702
|
}
|
|
703
|
+
this._eventListeners.clear();
|
|
540
704
|
}
|
|
541
705
|
// Getters
|
|
542
706
|
get isPlaying() {
|
|
@@ -552,7 +716,7 @@ var MediaElementPlayout = class {
|
|
|
552
716
|
return this.track?.duration ?? 0;
|
|
553
717
|
}
|
|
554
718
|
get sampleRate() {
|
|
555
|
-
return this.track?.peaks
|
|
719
|
+
return this.track?.peaks?.sample_rate ?? 44100;
|
|
556
720
|
}
|
|
557
721
|
/**
|
|
558
722
|
* Get the volume GainNode output for connecting external effects chains.
|