@waveform-playlist/media-element-playout 12.1.0 → 12.3.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 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.play(); // Resume
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
@@ -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
@@ -13,7 +13,7 @@ interface MediaElementTrackOptions {
13
13
  name?: string;
14
14
  /** Initial volume (0.0 to 1.0) */
15
15
  volume?: number;
16
- /** Initial playback rate (0.5 to 2.0) */
16
+ /** Initial playback rate (0.25 to 4.0) */
17
17
  playbackRate?: number;
18
18
  /** Whether to preserve pitch when changing playback rate (default: true) */
19
19
  preservesPitch?: boolean;
@@ -34,11 +34,30 @@ interface MediaElementTrackOptions {
34
34
  /** Fade out configuration (requires audioContext) */
35
35
  fadeOut?: FadeConfig;
36
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
+ }
37
56
  /**
38
57
  * Single-track playback using HTMLAudioElement.
39
58
  *
40
59
  * Benefits over AudioBuffer/Tone.js:
41
- * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm
60
+ * - Pitch-preserving playback rate (0.25x - 4.0x) via browser's built-in algorithm
42
61
  * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)
43
62
  * - Simpler, lighter-weight for single-track use cases
44
63
  *
@@ -57,6 +76,7 @@ declare class MediaElementTrack {
57
76
  private _volume;
58
77
  private onStopCallback?;
59
78
  private onTimeUpdateCallback?;
79
+ private _listeners;
60
80
  private _audioContext;
61
81
  private _sourceNode;
62
82
  private _fadeGain;
@@ -66,6 +86,10 @@ declare class MediaElementTrack {
66
86
  constructor(options: MediaElementTrackOptions);
67
87
  private handleEnded;
68
88
  private handleTimeUpdate;
89
+ private handleLoadedMetadata;
90
+ private handlePlay;
91
+ private handlePause;
92
+ private handleError;
69
93
  /**
70
94
  * Schedule fade automation on the fade GainNode.
71
95
  * Called at the start of each play() — fades are relative to the playback offset.
@@ -81,6 +105,29 @@ declare class MediaElementTrack {
81
105
  * (fades depend on audioContext.currentTime being non-zero).
82
106
  */
83
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;
84
131
  /**
85
132
  * Pause playback
86
133
  */
@@ -98,7 +145,7 @@ declare class MediaElementTrack {
98
145
  */
99
146
  setVolume(volume: number): void;
100
147
  /**
101
- * Set playback rate (0.5 to 2.0, pitch preserved)
148
+ * Set playback rate (0.25 to 4.0, pitch preserved)
102
149
  */
103
150
  setPlaybackRate(rate: number): void;
104
151
  /**
@@ -132,6 +179,16 @@ declare class MediaElementTrack {
132
179
  * Disconnect the output and reconnect to the default AudioContext destination.
133
180
  */
134
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;
135
192
  /**
136
193
  * Clean up resources
137
194
  */
@@ -159,7 +216,7 @@ declare class MediaElementTrack {
159
216
  interface MediaElementPlayoutOptions {
160
217
  /** Initial master volume (0.0 to 1.0) */
161
218
  masterVolume?: number;
162
- /** Initial playback rate (0.5 to 2.0) */
219
+ /** Initial playback rate (0.25 to 4.0) */
163
220
  playbackRate?: number;
164
221
  /** Whether to preserve pitch when changing playback rate (default: true).
165
222
  * Set to false when using an external pitch processor like SoundTouch. */
@@ -172,7 +229,7 @@ interface MediaElementPlayoutOptions {
172
229
  * that need pitch-preserving playback rate control.
173
230
  *
174
231
  * Key features:
175
- * - Pitch-preserving playback rate (0.5x - 2.0x)
232
+ * - Pitch-preserving playback rate (0.25x - 4.0x)
176
233
  * - Uses pre-computed peaks (no AudioBuffer required)
177
234
  * - Simpler API for single-track playback
178
235
  *
@@ -189,6 +246,8 @@ declare class MediaElementPlayout {
189
246
  private _preservesPitch;
190
247
  private _isPlaying;
191
248
  private onPlaybackCompleteCallback?;
249
+ /** Consumer event listeners, retained so they re-attach across track swaps. */
250
+ private _eventListeners;
192
251
  constructor(options?: MediaElementPlayoutOptions);
193
252
  /**
194
253
  * Initialize the playout engine.
@@ -202,6 +261,16 @@ declare class MediaElementPlayout {
202
261
  * Note: Only one track is supported. Adding a second track will dispose the first.
203
262
  */
204
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;
205
274
  /**
206
275
  * Remove a track by ID.
207
276
  */
@@ -217,6 +286,13 @@ declare class MediaElementPlayout {
217
286
  * @param duration - Duration to play in seconds (optional)
218
287
  */
219
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;
220
296
  /**
221
297
  * Pause playback.
222
298
  */
@@ -238,7 +314,7 @@ declare class MediaElementPlayout {
238
314
  */
239
315
  setMasterVolume(volume: number): void;
240
316
  /**
241
- * Set playback rate (0.5 to 2.0, pitch preserved).
317
+ * Set playback rate (0.25 to 4.0, pitch preserved).
242
318
  */
243
319
  setPlaybackRate(rate: number): void;
244
320
  /**
@@ -254,6 +330,23 @@ declare class MediaElementPlayout {
254
330
  * Set callback for when playback completes.
255
331
  */
256
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;
257
350
  /**
258
351
  * Clean up resources.
259
352
  */
@@ -307,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
307
400
  */
308
401
  declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
309
402
 
310
- 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
@@ -13,7 +13,7 @@ interface MediaElementTrackOptions {
13
13
  name?: string;
14
14
  /** Initial volume (0.0 to 1.0) */
15
15
  volume?: number;
16
- /** Initial playback rate (0.5 to 2.0) */
16
+ /** Initial playback rate (0.25 to 4.0) */
17
17
  playbackRate?: number;
18
18
  /** Whether to preserve pitch when changing playback rate (default: true) */
19
19
  preservesPitch?: boolean;
@@ -34,11 +34,30 @@ interface MediaElementTrackOptions {
34
34
  /** Fade out configuration (requires audioContext) */
35
35
  fadeOut?: FadeConfig;
36
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
+ }
37
56
  /**
38
57
  * Single-track playback using HTMLAudioElement.
39
58
  *
40
59
  * Benefits over AudioBuffer/Tone.js:
41
- * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm
60
+ * - Pitch-preserving playback rate (0.25x - 4.0x) via browser's built-in algorithm
42
61
  * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)
43
62
  * - Simpler, lighter-weight for single-track use cases
44
63
  *
@@ -57,6 +76,7 @@ declare class MediaElementTrack {
57
76
  private _volume;
58
77
  private onStopCallback?;
59
78
  private onTimeUpdateCallback?;
79
+ private _listeners;
60
80
  private _audioContext;
61
81
  private _sourceNode;
62
82
  private _fadeGain;
@@ -66,6 +86,10 @@ declare class MediaElementTrack {
66
86
  constructor(options: MediaElementTrackOptions);
67
87
  private handleEnded;
68
88
  private handleTimeUpdate;
89
+ private handleLoadedMetadata;
90
+ private handlePlay;
91
+ private handlePause;
92
+ private handleError;
69
93
  /**
70
94
  * Schedule fade automation on the fade GainNode.
71
95
  * Called at the start of each play() — fades are relative to the playback offset.
@@ -81,6 +105,29 @@ declare class MediaElementTrack {
81
105
  * (fades depend on audioContext.currentTime being non-zero).
82
106
  */
83
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;
84
131
  /**
85
132
  * Pause playback
86
133
  */
@@ -98,7 +145,7 @@ declare class MediaElementTrack {
98
145
  */
99
146
  setVolume(volume: number): void;
100
147
  /**
101
- * Set playback rate (0.5 to 2.0, pitch preserved)
148
+ * Set playback rate (0.25 to 4.0, pitch preserved)
102
149
  */
103
150
  setPlaybackRate(rate: number): void;
104
151
  /**
@@ -132,6 +179,16 @@ declare class MediaElementTrack {
132
179
  * Disconnect the output and reconnect to the default AudioContext destination.
133
180
  */
134
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;
135
192
  /**
136
193
  * Clean up resources
137
194
  */
@@ -159,7 +216,7 @@ declare class MediaElementTrack {
159
216
  interface MediaElementPlayoutOptions {
160
217
  /** Initial master volume (0.0 to 1.0) */
161
218
  masterVolume?: number;
162
- /** Initial playback rate (0.5 to 2.0) */
219
+ /** Initial playback rate (0.25 to 4.0) */
163
220
  playbackRate?: number;
164
221
  /** Whether to preserve pitch when changing playback rate (default: true).
165
222
  * Set to false when using an external pitch processor like SoundTouch. */
@@ -172,7 +229,7 @@ interface MediaElementPlayoutOptions {
172
229
  * that need pitch-preserving playback rate control.
173
230
  *
174
231
  * Key features:
175
- * - Pitch-preserving playback rate (0.5x - 2.0x)
232
+ * - Pitch-preserving playback rate (0.25x - 4.0x)
176
233
  * - Uses pre-computed peaks (no AudioBuffer required)
177
234
  * - Simpler API for single-track playback
178
235
  *
@@ -189,6 +246,8 @@ declare class MediaElementPlayout {
189
246
  private _preservesPitch;
190
247
  private _isPlaying;
191
248
  private onPlaybackCompleteCallback?;
249
+ /** Consumer event listeners, retained so they re-attach across track swaps. */
250
+ private _eventListeners;
192
251
  constructor(options?: MediaElementPlayoutOptions);
193
252
  /**
194
253
  * Initialize the playout engine.
@@ -202,6 +261,16 @@ declare class MediaElementPlayout {
202
261
  * Note: Only one track is supported. Adding a second track will dispose the first.
203
262
  */
204
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;
205
274
  /**
206
275
  * Remove a track by ID.
207
276
  */
@@ -217,6 +286,13 @@ declare class MediaElementPlayout {
217
286
  * @param duration - Duration to play in seconds (optional)
218
287
  */
219
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;
220
296
  /**
221
297
  * Pause playback.
222
298
  */
@@ -238,7 +314,7 @@ declare class MediaElementPlayout {
238
314
  */
239
315
  setMasterVolume(volume: number): void;
240
316
  /**
241
- * Set playback rate (0.5 to 2.0, pitch preserved).
317
+ * Set playback rate (0.25 to 4.0, pitch preserved).
242
318
  */
243
319
  setPlaybackRate(rate: number): void;
244
320
  /**
@@ -254,6 +330,23 @@ declare class MediaElementPlayout {
254
330
  * Set callback for when playback completes.
255
331
  */
256
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;
257
350
  /**
258
351
  * Clean up resources.
259
352
  */
@@ -307,4 +400,4 @@ interface PlaybackRateEngine extends PlayoutEngine {
307
400
  */
308
401
  declare function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine;
309
402
 
310
- 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 };