@waveform-playlist/media-element-playout 5.0.0-alpha.14
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/LICENSE.md +21 -0
- package/README.md +137 -0
- package/dist/index.d.mts +235 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +369 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +340 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// src/MediaElementTrack.ts
|
|
2
|
+
var MediaElementTrack = class {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this._playbackRate = 1;
|
|
5
|
+
this.handleEnded = () => {
|
|
6
|
+
if (this.onStopCallback) {
|
|
7
|
+
this.onStopCallback();
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
this.handleTimeUpdate = () => {
|
|
11
|
+
if (this.onTimeUpdateCallback) {
|
|
12
|
+
this.onTimeUpdateCallback(this.audioElement.currentTime);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
this._peaks = options.peaks;
|
|
16
|
+
this._id = options.id ?? `track-${Date.now()}`;
|
|
17
|
+
this._name = options.name ?? "Track";
|
|
18
|
+
this._playbackRate = options.playbackRate ?? 1;
|
|
19
|
+
if (typeof options.source === "string") {
|
|
20
|
+
this.audioElement = new Audio(options.source);
|
|
21
|
+
this.ownsElement = true;
|
|
22
|
+
} else {
|
|
23
|
+
this.audioElement = options.source;
|
|
24
|
+
this.ownsElement = false;
|
|
25
|
+
}
|
|
26
|
+
this.audioElement.preload = "auto";
|
|
27
|
+
this.audioElement.volume = options.volume ?? 1;
|
|
28
|
+
this.audioElement.playbackRate = this._playbackRate;
|
|
29
|
+
if ("preservesPitch" in this.audioElement) {
|
|
30
|
+
this.audioElement.preservesPitch = true;
|
|
31
|
+
} else if ("mozPreservesPitch" in this.audioElement) {
|
|
32
|
+
this.audioElement.mozPreservesPitch = true;
|
|
33
|
+
} else if ("webkitPreservesPitch" in this.audioElement) {
|
|
34
|
+
this.audioElement.webkitPreservesPitch = true;
|
|
35
|
+
}
|
|
36
|
+
this.audioElement.addEventListener("ended", this.handleEnded);
|
|
37
|
+
this.audioElement.addEventListener("timeupdate", this.handleTimeUpdate);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start playback from a specific time
|
|
41
|
+
*/
|
|
42
|
+
play(offset = 0) {
|
|
43
|
+
this.audioElement.currentTime = offset;
|
|
44
|
+
this.audioElement.play().catch((err) => {
|
|
45
|
+
console.warn("MediaElementTrack: play() failed:", err);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pause playback
|
|
50
|
+
*/
|
|
51
|
+
pause() {
|
|
52
|
+
this.audioElement.pause();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stop playback and reset to beginning
|
|
56
|
+
*/
|
|
57
|
+
stop() {
|
|
58
|
+
this.audioElement.pause();
|
|
59
|
+
this.audioElement.currentTime = 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Seek to a specific time
|
|
63
|
+
*/
|
|
64
|
+
seekTo(time) {
|
|
65
|
+
this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Set volume (0.0 to 1.0)
|
|
69
|
+
*/
|
|
70
|
+
setVolume(volume) {
|
|
71
|
+
this.audioElement.volume = Math.max(0, Math.min(1, volume));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Set playback rate (0.5 to 2.0, pitch preserved)
|
|
75
|
+
*/
|
|
76
|
+
setPlaybackRate(rate) {
|
|
77
|
+
const clampedRate = Math.max(0.5, Math.min(2, rate));
|
|
78
|
+
this._playbackRate = clampedRate;
|
|
79
|
+
this.audioElement.playbackRate = clampedRate;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Set muted state
|
|
83
|
+
*/
|
|
84
|
+
setMuted(muted) {
|
|
85
|
+
this.audioElement.muted = muted;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Set callback for when playback ends
|
|
89
|
+
*/
|
|
90
|
+
setOnStopCallback(callback) {
|
|
91
|
+
this.onStopCallback = callback;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Set callback for time updates
|
|
95
|
+
*/
|
|
96
|
+
setOnTimeUpdateCallback(callback) {
|
|
97
|
+
this.onTimeUpdateCallback = callback;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clean up resources
|
|
101
|
+
*/
|
|
102
|
+
dispose() {
|
|
103
|
+
this.audioElement.removeEventListener("ended", this.handleEnded);
|
|
104
|
+
this.audioElement.removeEventListener("timeupdate", this.handleTimeUpdate);
|
|
105
|
+
this.audioElement.pause();
|
|
106
|
+
if (this.ownsElement) {
|
|
107
|
+
this.audioElement.src = "";
|
|
108
|
+
this.audioElement.load();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Getters
|
|
112
|
+
get id() {
|
|
113
|
+
return this._id;
|
|
114
|
+
}
|
|
115
|
+
get name() {
|
|
116
|
+
return this._name;
|
|
117
|
+
}
|
|
118
|
+
get peaks() {
|
|
119
|
+
return this._peaks;
|
|
120
|
+
}
|
|
121
|
+
get currentTime() {
|
|
122
|
+
return this.audioElement.currentTime;
|
|
123
|
+
}
|
|
124
|
+
get duration() {
|
|
125
|
+
return this.audioElement.duration || this._peaks.duration;
|
|
126
|
+
}
|
|
127
|
+
get isPlaying() {
|
|
128
|
+
return !this.audioElement.paused && !this.audioElement.ended;
|
|
129
|
+
}
|
|
130
|
+
get volume() {
|
|
131
|
+
return this.audioElement.volume;
|
|
132
|
+
}
|
|
133
|
+
get playbackRate() {
|
|
134
|
+
return this._playbackRate;
|
|
135
|
+
}
|
|
136
|
+
get muted() {
|
|
137
|
+
return this.audioElement.muted;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get the underlying audio element (for advanced use cases)
|
|
141
|
+
*/
|
|
142
|
+
get element() {
|
|
143
|
+
return this.audioElement;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/MediaElementPlayout.ts
|
|
148
|
+
var MediaElementPlayout = class {
|
|
149
|
+
constructor(options = {}) {
|
|
150
|
+
this.track = null;
|
|
151
|
+
this._isPlaying = false;
|
|
152
|
+
this._masterVolume = options.masterVolume ?? 1;
|
|
153
|
+
this._playbackRate = options.playbackRate ?? 1;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Initialize the playout engine.
|
|
157
|
+
* For MediaElementPlayout this is a no-op (no AudioContext to start).
|
|
158
|
+
*/
|
|
159
|
+
async init() {
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Add a track to the playout.
|
|
163
|
+
* Note: Only one track is supported. Adding a second track will dispose the first.
|
|
164
|
+
*/
|
|
165
|
+
addTrack(options) {
|
|
166
|
+
if (this.track) {
|
|
167
|
+
console.warn(
|
|
168
|
+
"MediaElementPlayout: Only one track is supported. Disposing previous track. For multi-track, use TonePlayout."
|
|
169
|
+
);
|
|
170
|
+
this.track.dispose();
|
|
171
|
+
}
|
|
172
|
+
this.track = new MediaElementTrack({
|
|
173
|
+
...options,
|
|
174
|
+
volume: this._masterVolume * (options.volume ?? 1),
|
|
175
|
+
playbackRate: this._playbackRate
|
|
176
|
+
});
|
|
177
|
+
this.track.setOnStopCallback(() => {
|
|
178
|
+
this._isPlaying = false;
|
|
179
|
+
if (this.onPlaybackCompleteCallback) {
|
|
180
|
+
this.onPlaybackCompleteCallback();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return this.track;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Remove a track by ID.
|
|
187
|
+
*/
|
|
188
|
+
removeTrack(trackId) {
|
|
189
|
+
if (this.track && this.track.id === trackId) {
|
|
190
|
+
this.track.dispose();
|
|
191
|
+
this.track = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get a track by ID.
|
|
196
|
+
*/
|
|
197
|
+
getTrack(trackId) {
|
|
198
|
+
if (this.track && this.track.id === trackId) {
|
|
199
|
+
return this.track;
|
|
200
|
+
}
|
|
201
|
+
return void 0;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Start playback.
|
|
205
|
+
* @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)
|
|
206
|
+
* @param offset - Start position in seconds
|
|
207
|
+
* @param duration - Duration to play in seconds (optional)
|
|
208
|
+
*/
|
|
209
|
+
play(_when, offset, duration) {
|
|
210
|
+
if (!this.track) {
|
|
211
|
+
console.warn("MediaElementPlayout: No track to play");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const startPosition = offset ?? 0;
|
|
215
|
+
this._isPlaying = true;
|
|
216
|
+
this.track.play(startPosition);
|
|
217
|
+
if (duration !== void 0) {
|
|
218
|
+
const adjustedDuration = duration / this._playbackRate;
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
if (this._isPlaying) {
|
|
221
|
+
this.pause();
|
|
222
|
+
if (this.onPlaybackCompleteCallback) {
|
|
223
|
+
this.onPlaybackCompleteCallback();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}, adjustedDuration * 1e3);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Pause playback.
|
|
231
|
+
*/
|
|
232
|
+
pause() {
|
|
233
|
+
if (this.track) {
|
|
234
|
+
this.track.pause();
|
|
235
|
+
}
|
|
236
|
+
this._isPlaying = false;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Stop playback and reset to start.
|
|
240
|
+
*/
|
|
241
|
+
stop() {
|
|
242
|
+
if (this.track) {
|
|
243
|
+
this.track.stop();
|
|
244
|
+
}
|
|
245
|
+
this._isPlaying = false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Seek to a specific time.
|
|
249
|
+
*/
|
|
250
|
+
seekTo(time) {
|
|
251
|
+
if (this.track) {
|
|
252
|
+
this.track.seekTo(time);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get current playback time.
|
|
257
|
+
*/
|
|
258
|
+
getCurrentTime() {
|
|
259
|
+
if (this.track) {
|
|
260
|
+
return this.track.currentTime;
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Set master volume.
|
|
266
|
+
*/
|
|
267
|
+
setMasterVolume(volume) {
|
|
268
|
+
this._masterVolume = Math.max(0, Math.min(1, volume));
|
|
269
|
+
if (this.track) {
|
|
270
|
+
this.track.setVolume(this._masterVolume);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Set playback rate (0.5 to 2.0, pitch preserved).
|
|
275
|
+
*/
|
|
276
|
+
setPlaybackRate(rate) {
|
|
277
|
+
this._playbackRate = Math.max(0.5, Math.min(2, rate));
|
|
278
|
+
if (this.track) {
|
|
279
|
+
this.track.setPlaybackRate(this._playbackRate);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Set mute state for a track.
|
|
284
|
+
*/
|
|
285
|
+
setMute(trackId, muted) {
|
|
286
|
+
const track = this.getTrack(trackId);
|
|
287
|
+
if (track) {
|
|
288
|
+
track.setMuted(muted);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Set solo state for a track.
|
|
293
|
+
* Note: With single track, solo is effectively the same as unmute.
|
|
294
|
+
*/
|
|
295
|
+
setSolo(_trackId, _soloed) {
|
|
296
|
+
console.warn("MediaElementPlayout: Solo is not applicable for single-track playback");
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Set callback for when playback completes.
|
|
300
|
+
*/
|
|
301
|
+
setOnPlaybackComplete(callback) {
|
|
302
|
+
this.onPlaybackCompleteCallback = callback;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Clean up resources.
|
|
306
|
+
*/
|
|
307
|
+
dispose() {
|
|
308
|
+
if (this.track) {
|
|
309
|
+
this.track.dispose();
|
|
310
|
+
this.track = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Getters
|
|
314
|
+
get isPlaying() {
|
|
315
|
+
return this._isPlaying;
|
|
316
|
+
}
|
|
317
|
+
get masterVolume() {
|
|
318
|
+
return this._masterVolume;
|
|
319
|
+
}
|
|
320
|
+
get playbackRate() {
|
|
321
|
+
return this._playbackRate;
|
|
322
|
+
}
|
|
323
|
+
get duration() {
|
|
324
|
+
return this.track?.duration ?? 0;
|
|
325
|
+
}
|
|
326
|
+
get sampleRate() {
|
|
327
|
+
return this.track?.peaks.sample_rate ?? 44100;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/types.ts
|
|
332
|
+
function supportsPlaybackRate(engine) {
|
|
333
|
+
return "setPlaybackRate" in engine && typeof engine.setPlaybackRate === "function";
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
MediaElementPlayout,
|
|
337
|
+
MediaElementTrack,
|
|
338
|
+
supportsPlaybackRate
|
|
339
|
+
};
|
|
340
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MediaElementTrack.ts","../src/MediaElementPlayout.ts","../src/types.ts"],"sourcesContent":["import type { WaveformDataObject } from '@waveform-playlist/core';\n\nexport interface MediaElementTrackOptions {\n /** The audio source - can be a URL, Blob URL, or HTMLAudioElement */\n source: string | HTMLAudioElement;\n /** Pre-computed waveform data for visualization (required - no AudioBuffer decoding) */\n peaks: WaveformDataObject;\n /** Track ID */\n id?: string;\n /** Track name for display */\n name?: string;\n /** Initial volume (0.0 to 1.0) */\n volume?: number;\n /** Initial playback rate (0.5 to 2.0, pitch preserved) */\n playbackRate?: number;\n}\n\n/**\n * Single-track playback using HTMLAudioElement.\n *\n * Benefits over AudioBuffer/Tone.js:\n * - Pitch-preserving playback rate (0.5x - 2.0x) via browser's built-in algorithm\n * - No AudioBuffer decoding required (uses pre-computed peaks for visualization)\n * - Simpler, lighter-weight for single-track use cases\n *\n * Limitations:\n * - Single track only (no multi-track mixing)\n * - No clip-level effects or fades (track-level volume only)\n * - Relies on browser's time-stretching quality\n */\nexport class MediaElementTrack {\n private audioElement: HTMLAudioElement;\n private ownsElement: boolean; // Whether we created the element (need to clean up)\n private _peaks: WaveformDataObject;\n private _id: string;\n private _name: string;\n private _playbackRate: number = 1;\n private onStopCallback?: () => void;\n private onTimeUpdateCallback?: (time: number) => void;\n\n constructor(options: MediaElementTrackOptions) {\n this._peaks = options.peaks;\n this._id = options.id ?? `track-${Date.now()}`;\n this._name = options.name ?? 'Track';\n this._playbackRate = options.playbackRate ?? 1;\n\n // Create or use provided audio element\n if (typeof options.source === 'string') {\n this.audioElement = new Audio(options.source);\n this.ownsElement = true;\n } else {\n this.audioElement = options.source;\n this.ownsElement = false;\n }\n\n // Configure audio element\n this.audioElement.preload = 'auto';\n this.audioElement.volume = options.volume ?? 1;\n this.audioElement.playbackRate = this._playbackRate;\n\n // Preserve pitch when changing playback rate (default in modern browsers)\n // Some older browsers may not support this, but it's the default behavior\n if ('preservesPitch' in this.audioElement) {\n (this.audioElement as any).preservesPitch = true;\n } else if ('mozPreservesPitch' in this.audioElement) {\n // Firefox prefix\n (this.audioElement as any).mozPreservesPitch = true;\n } else if ('webkitPreservesPitch' in this.audioElement) {\n // Safari prefix\n (this.audioElement as any).webkitPreservesPitch = true;\n }\n\n // Set up event listeners\n this.audioElement.addEventListener('ended', this.handleEnded);\n this.audioElement.addEventListener('timeupdate', this.handleTimeUpdate);\n }\n\n private handleEnded = () => {\n if (this.onStopCallback) {\n this.onStopCallback();\n }\n };\n\n private handleTimeUpdate = () => {\n if (this.onTimeUpdateCallback) {\n this.onTimeUpdateCallback(this.audioElement.currentTime);\n }\n };\n\n /**\n * Start playback from a specific time\n */\n play(offset: number = 0): void {\n this.audioElement.currentTime = offset;\n this.audioElement.play().catch(err => {\n console.warn('MediaElementTrack: play() failed:', err);\n });\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n this.audioElement.pause();\n }\n\n /**\n * Stop playback and reset to beginning\n */\n stop(): void {\n this.audioElement.pause();\n this.audioElement.currentTime = 0;\n }\n\n /**\n * Seek to a specific time\n */\n seekTo(time: number): void {\n this.audioElement.currentTime = Math.max(0, Math.min(time, this.duration));\n }\n\n /**\n * Set volume (0.0 to 1.0)\n */\n setVolume(volume: number): void {\n this.audioElement.volume = Math.max(0, Math.min(1, volume));\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved)\n */\n setPlaybackRate(rate: number): void {\n // Clamp to reasonable range for pitch preservation quality\n const clampedRate = Math.max(0.5, Math.min(2.0, rate));\n this._playbackRate = clampedRate;\n this.audioElement.playbackRate = clampedRate;\n }\n\n /**\n * Set muted state\n */\n setMuted(muted: boolean): void {\n this.audioElement.muted = muted;\n }\n\n /**\n * Set callback for when playback ends\n */\n setOnStopCallback(callback: () => void): void {\n this.onStopCallback = callback;\n }\n\n /**\n * Set callback for time updates\n */\n setOnTimeUpdateCallback(callback: (time: number) => void): void {\n this.onTimeUpdateCallback = callback;\n }\n\n /**\n * Clean up resources\n */\n dispose(): void {\n this.audioElement.removeEventListener('ended', this.handleEnded);\n this.audioElement.removeEventListener('timeupdate', this.handleTimeUpdate);\n this.audioElement.pause();\n\n if (this.ownsElement) {\n this.audioElement.src = '';\n this.audioElement.load(); // Release resources\n }\n }\n\n // Getters\n get id(): string {\n return this._id;\n }\n\n get name(): string {\n return this._name;\n }\n\n get peaks(): WaveformDataObject {\n return this._peaks;\n }\n\n get currentTime(): number {\n return this.audioElement.currentTime;\n }\n\n get duration(): number {\n return this.audioElement.duration || this._peaks.duration;\n }\n\n get isPlaying(): boolean {\n return !this.audioElement.paused && !this.audioElement.ended;\n }\n\n get volume(): number {\n return this.audioElement.volume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get muted(): boolean {\n return this.audioElement.muted;\n }\n\n /**\n * Get the underlying audio element (for advanced use cases)\n */\n get element(): HTMLAudioElement {\n return this.audioElement;\n }\n}\n","import { MediaElementTrack, type MediaElementTrackOptions } from './MediaElementTrack';\n\nexport interface MediaElementPlayoutOptions {\n /** Initial master volume (0.0 to 1.0) */\n masterVolume?: number;\n /** Initial playback rate (0.5 to 2.0) */\n playbackRate?: number;\n}\n\n/**\n * Single-track playout engine using HTMLAudioElement.\n *\n * This is a lightweight alternative to TonePlayout for single-track use cases\n * that need pitch-preserving playback rate control.\n *\n * Key features:\n * - Pitch-preserving playback rate (0.5x - 2.0x)\n * - Uses pre-computed peaks (no AudioBuffer required)\n * - Simpler API for single-track playback\n *\n * Limitations:\n * - Single track only - will warn if multiple tracks added\n * - No clip-level effects or crossfades\n * - No multi-track mixing\n *\n * For multi-track editing, use TonePlayout from @waveform-playlist/playout instead.\n */\nexport class MediaElementPlayout {\n private track: MediaElementTrack | null = null;\n private _masterVolume: number;\n private _playbackRate: number;\n private _isPlaying: boolean = false;\n private onPlaybackCompleteCallback?: () => void;\n\n constructor(options: MediaElementPlayoutOptions = {}) {\n this._masterVolume = options.masterVolume ?? 1;\n this._playbackRate = options.playbackRate ?? 1;\n }\n\n /**\n * Initialize the playout engine.\n * For MediaElementPlayout this is a no-op (no AudioContext to start).\n */\n async init(): Promise<void> {\n // No initialization needed for HTMLAudioElement\n // AudioContext requires user gesture, but audio element just works\n }\n\n /**\n * Add a track to the playout.\n * Note: Only one track is supported. Adding a second track will dispose the first.\n */\n addTrack(options: MediaElementTrackOptions): MediaElementTrack {\n if (this.track) {\n console.warn(\n 'MediaElementPlayout: Only one track is supported. ' +\n 'Disposing previous track. For multi-track, use TonePlayout.'\n );\n this.track.dispose();\n }\n\n this.track = new MediaElementTrack({\n ...options,\n volume: this._masterVolume * (options.volume ?? 1),\n playbackRate: this._playbackRate,\n });\n\n // Set up stop callback\n this.track.setOnStopCallback(() => {\n this._isPlaying = false;\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n });\n\n return this.track;\n }\n\n /**\n * Remove a track by ID.\n */\n removeTrack(trackId: string): void {\n if (this.track && this.track.id === trackId) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n /**\n * Get a track by ID.\n */\n getTrack(trackId: string): MediaElementTrack | undefined {\n if (this.track && this.track.id === trackId) {\n return this.track;\n }\n return undefined;\n }\n\n /**\n * Start playback.\n * @param _when - Ignored (HTMLAudioElement doesn't support scheduled start)\n * @param offset - Start position in seconds\n * @param duration - Duration to play in seconds (optional)\n */\n play(_when?: number, offset?: number, duration?: number): void {\n if (!this.track) {\n console.warn('MediaElementPlayout: No track to play');\n return;\n }\n\n const startPosition = offset ?? 0;\n this._isPlaying = true;\n\n this.track.play(startPosition);\n\n // If duration is specified, schedule stop\n if (duration !== undefined) {\n const adjustedDuration = duration / this._playbackRate;\n setTimeout(() => {\n if (this._isPlaying) {\n this.pause();\n if (this.onPlaybackCompleteCallback) {\n this.onPlaybackCompleteCallback();\n }\n }\n }, adjustedDuration * 1000);\n }\n }\n\n /**\n * Pause playback.\n */\n pause(): void {\n if (this.track) {\n this.track.pause();\n }\n this._isPlaying = false;\n }\n\n /**\n * Stop playback and reset to start.\n */\n stop(): void {\n if (this.track) {\n this.track.stop();\n }\n this._isPlaying = false;\n }\n\n /**\n * Seek to a specific time.\n */\n seekTo(time: number): void {\n if (this.track) {\n this.track.seekTo(time);\n }\n }\n\n /**\n * Get current playback time.\n */\n getCurrentTime(): number {\n if (this.track) {\n return this.track.currentTime;\n }\n return 0;\n }\n\n /**\n * Set master volume.\n */\n setMasterVolume(volume: number): void {\n this._masterVolume = Math.max(0, Math.min(1, volume));\n if (this.track) {\n this.track.setVolume(this._masterVolume);\n }\n }\n\n /**\n * Set playback rate (0.5 to 2.0, pitch preserved).\n */\n setPlaybackRate(rate: number): void {\n this._playbackRate = Math.max(0.5, Math.min(2.0, rate));\n if (this.track) {\n this.track.setPlaybackRate(this._playbackRate);\n }\n }\n\n /**\n * Set mute state for a track.\n */\n setMute(trackId: string, muted: boolean): void {\n const track = this.getTrack(trackId);\n if (track) {\n track.setMuted(muted);\n }\n }\n\n /**\n * Set solo state for a track.\n * Note: With single track, solo is effectively the same as unmute.\n */\n setSolo(_trackId: string, _soloed: boolean): void {\n // No-op for single track - solo doesn't make sense\n console.warn('MediaElementPlayout: Solo is not applicable for single-track playback');\n }\n\n /**\n * Set callback for when playback completes.\n */\n setOnPlaybackComplete(callback: () => void): void {\n this.onPlaybackCompleteCallback = callback;\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.track) {\n this.track.dispose();\n this.track = null;\n }\n }\n\n // Getters\n get isPlaying(): boolean {\n return this._isPlaying;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get duration(): number {\n return this.track?.duration ?? 0;\n }\n\n get sampleRate(): number {\n // HTMLAudioElement doesn't expose sample rate directly\n // Return a common default - peaks will have the actual sample rate\n return this.track?.peaks.sample_rate ?? 44100;\n }\n}\n","/**\n * Common interface for playout engines.\n *\n * Both TonePlayout and MediaElementPlayout implement this interface,\n * allowing the browser package to work with either engine.\n */\nexport interface PlayoutEngine {\n // Lifecycle\n init(): Promise<void>;\n dispose(): void;\n\n // Playback\n play(when?: number, offset?: number, duration?: number): void;\n pause(): void;\n stop(): void;\n seekTo(time: number): void;\n getCurrentTime(): number;\n\n // Volume\n setMasterVolume(volume: number): void;\n\n // Track controls (optional - not all engines support all features)\n setMute?(trackId: string, muted: boolean): void;\n setSolo?(trackId: string, soloed: boolean): void;\n\n // Callbacks\n setOnPlaybackComplete(callback: () => void): void;\n\n // State\n readonly isPlaying: boolean;\n readonly duration: number;\n readonly sampleRate: number;\n}\n\n/**\n * Extended interface for engines that support playback rate.\n */\nexport interface PlaybackRateEngine extends PlayoutEngine {\n setPlaybackRate(rate: number): void;\n readonly playbackRate: number;\n}\n\n/**\n * Type guard to check if an engine supports playback rate.\n */\nexport function supportsPlaybackRate(engine: PlayoutEngine): engine is PlaybackRateEngine {\n return 'setPlaybackRate' in engine && typeof (engine as any).setPlaybackRate === 'function';\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waveform-playlist/media-element-playout",
|
|
3
|
+
"version": "5.0.0-alpha.14",
|
|
4
|
+
"description": "HTMLMediaElement-based playout engine for waveform-playlist with pitch-preserving playback rate",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"keywords": [
|
|
17
|
+
"waveform",
|
|
18
|
+
"audio",
|
|
19
|
+
"webaudio",
|
|
20
|
+
"playback-rate",
|
|
21
|
+
"waveform-playlist",
|
|
22
|
+
"html-audio"
|
|
23
|
+
],
|
|
24
|
+
"author": "Naomi Aro",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/naomiaro/waveform-playlist.git",
|
|
29
|
+
"directory": "packages/media-element-playout"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://naomiaro.github.io/waveform-playlist",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/naomiaro/waveform-playlist/issues"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsup": "^8.0.1",
|
|
41
|
+
"typescript": "^5.3.3"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@waveform-playlist/core": "5.0.0-alpha.14"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|