wavesurfer.js 7.0.0-alpha.2 → 7.0.0-alpha.21

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
@@ -1,7 +1,14 @@
1
- # wavesurfer.ts
1
+ # <img src="https://user-images.githubusercontent.com/381895/226091100-f5567a28-7736-4d37-8f84-e08f297b7e1a.png" alt="logo" height="60" valign="middle" /> wavesurfer.ts
2
2
 
3
3
  An experimental rewrite of [wavesufer.js](https://github.com/wavesurfer-js/wavesurfer.js) to play around with new ideas.
4
4
 
5
+ <img alt="screenshot" src="https://user-images.githubusercontent.com/381895/225539680-fc724acd-8657-458e-a558-ff1c6758ba30.png" width="800" />
6
+
7
+ Try it out:
8
+ ```
9
+ npm install --save wavesurfer.js@alpha
10
+ ```
11
+
5
12
  ## Goals
6
13
 
7
14
  * TypeScript API
@@ -17,11 +24,9 @@ Keeping backwards compatibility with earlier versions of wavesurfer.js.
17
24
 
18
25
  Principles:
19
26
  * Modular and event-driven
20
- * Allow swapping the decoding, rendering and playback mechanisms
27
+ * Flexible (e.g. allow custom media elements and user-defined Web Audio graphs)
21
28
  * Extensible with plugins
22
29
 
23
- ![diagram](https://user-images.githubusercontent.com/381895/222349436-38b550e5-24dc-4143-9cdb-efbe00540213.png)
24
-
25
30
  ## Development
26
31
 
27
32
  Install dev dependencies:
@@ -30,21 +35,14 @@ Install dev dependencies:
30
35
  yarn
31
36
  ```
32
37
 
33
- Start the TypeScript compiler in watch mode:
34
-
35
- ```
36
- yarn build:dev
37
- ```
38
-
39
- Run an HTTP server to view the examples:
38
+ Start the TypeScript compiler in watch mode and an HTTP server:
40
39
 
41
40
  ```
42
- python3 -m http.server --cgi 8080
41
+ yarn start
43
42
  ```
44
43
 
45
- Open http://localhost:8080/examples/basic/ in your browser.
46
- There's no hot reload yet, so you'll need to reload the page manually on every change.
44
+ This will open http://localhost:9090/tutorial in your browser with live reload.
47
45
 
48
46
  ## Feedback
49
47
 
50
- See https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
48
+ Your feedback is very welcome here: https://github.com/wavesurfer-js/wavesurfer.js/discussions/2684
@@ -1,10 +1,12 @@
1
1
  import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
2
- import type { WaveSurfer, WaveSurferPluginParams } from './index.js';
3
- export declare class BasePlugin<EventTypes extends GeneralEventTypes> extends EventEmitter<EventTypes> {
2
+ import WaveSurfer, { type WaveSurferPluginParams } from './index.js';
3
+ export declare class BasePlugin<EventTypes extends GeneralEventTypes, Options> extends EventEmitter<EventTypes> {
4
4
  protected wavesurfer: WaveSurfer;
5
- protected renderer: WaveSurfer['renderer'];
5
+ protected container: HTMLElement;
6
+ protected wrapper: HTMLElement;
6
7
  protected subscriptions: (() => void)[];
7
- constructor(params: WaveSurferPluginParams);
8
+ protected options: Options;
9
+ constructor(params: WaveSurferPluginParams, options: Options);
8
10
  destroy(): void;
9
11
  }
10
12
  export default BasePlugin;
@@ -0,0 +1,19 @@
1
+ import EventEmitter from './event-emitter.js';
2
+ export class BasePlugin extends EventEmitter {
3
+ wavesurfer;
4
+ container;
5
+ wrapper;
6
+ subscriptions = [];
7
+ options;
8
+ constructor(params, options) {
9
+ super();
10
+ this.wavesurfer = params.wavesurfer;
11
+ this.container = params.container;
12
+ this.wrapper = params.wrapper;
13
+ this.options = options;
14
+ }
15
+ destroy() {
16
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
17
+ }
18
+ }
19
+ export default BasePlugin;
package/dist/decoder.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  declare class Decoder {
2
2
  audioCtx: AudioContext | null;
3
+ private initAudioContext;
3
4
  constructor();
4
- decode(audioData: ArrayBuffer): Promise<{
5
- duration: number;
6
- channelData: Float32Array[];
7
- }>;
5
+ decode(audioData: ArrayBuffer): Promise<AudioBuffer>;
8
6
  destroy(): void;
9
7
  }
10
8
  export default Decoder;
@@ -0,0 +1,31 @@
1
+ class Decoder {
2
+ audioCtx = null;
3
+ initAudioContext(sampleRate) {
4
+ this.audioCtx = new AudioContext({
5
+ latencyHint: 'playback',
6
+ sampleRate,
7
+ });
8
+ }
9
+ constructor() {
10
+ // Minimum sample rate supported by Web Audio API
11
+ const DEFAULT_SAMPLE_RATE = 3000; // Chrome, Safari
12
+ const FALLBACK_SAMPLE_RATE = 8000; // Firefox
13
+ try {
14
+ this.initAudioContext(DEFAULT_SAMPLE_RATE);
15
+ }
16
+ catch (e) {
17
+ this.initAudioContext(FALLBACK_SAMPLE_RATE);
18
+ }
19
+ }
20
+ async decode(audioData) {
21
+ if (!this.audioCtx) {
22
+ throw new Error('AudioContext is not initialized');
23
+ }
24
+ return this.audioCtx.decodeAudioData(audioData);
25
+ }
26
+ destroy() {
27
+ this.audioCtx?.close();
28
+ this.audioCtx = null;
29
+ }
30
+ }
31
+ export default Decoder;
@@ -6,7 +6,7 @@ declare class EventEmitter<EventTypes extends GeneralEventTypes> {
6
6
  constructor();
7
7
  protected emit<T extends keyof EventTypes>(eventType: T, detail?: EventTypes[T]): void;
8
8
  /** Subscribe to an event and return a function to unsubscribe */
9
- on<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void): () => void;
9
+ on<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void, once?: boolean): () => void;
10
10
  /** Subscribe to an event once and return a function to unsubscribe */
11
11
  once<T extends keyof EventTypes>(eventType: T, callback: (detail: EventTypes[T]) => void): () => void;
12
12
  }
@@ -0,0 +1,26 @@
1
+ class EventEmitter {
2
+ eventTarget;
3
+ constructor() {
4
+ this.eventTarget = new EventTarget();
5
+ }
6
+ emit(eventType, detail) {
7
+ const e = new CustomEvent(String(eventType), { detail });
8
+ this.eventTarget.dispatchEvent(e);
9
+ }
10
+ /** Subscribe to an event and return a function to unsubscribe */
11
+ on(eventType, callback, once) {
12
+ const handler = (e) => {
13
+ if (e instanceof CustomEvent) {
14
+ callback(e.detail);
15
+ }
16
+ };
17
+ const eventName = String(eventType);
18
+ this.eventTarget.addEventListener(eventName, handler, { once });
19
+ return () => this.eventTarget.removeEventListener(eventName, handler);
20
+ }
21
+ /** Subscribe to an event once and return a function to unsubscribe */
22
+ once(eventType, callback) {
23
+ return this.on(eventType, callback, true);
24
+ }
25
+ }
26
+ export default EventEmitter;
@@ -0,0 +1,6 @@
1
+ class Fetcher {
2
+ async load(url) {
3
+ return fetch(url).then((response) => response.arrayBuffer());
4
+ }
5
+ }
6
+ export default Fetcher;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
2
2
  import BasePlugin from './base-plugin.js';
3
- export declare enum PlayerType {
4
- WebAudio = "WebAudio",
5
- MediaElement = "MediaElement"
6
- }
7
3
  export type WaveSurferOptions = {
8
4
  /** HTML element or CSS selector */
9
5
  container: HTMLElement | string | null;
@@ -13,6 +9,10 @@ export type WaveSurferOptions = {
13
9
  waveColor?: string;
14
10
  /** The color of the progress mask */
15
11
  progressColor?: string;
12
+ /** The color of the playpack cursor */
13
+ cursorColor?: string;
14
+ /** The cursor with */
15
+ cursorWidth?: number;
16
16
  /** If set, the waveform will be rendered in bars like so: ▁ ▂ ▇ ▃ ▅ ▂ */
17
17
  barWidth?: number;
18
18
  /** Spacing between bars in pixels */
@@ -21,37 +21,65 @@ export type WaveSurferOptions = {
21
21
  barRadius?: number;
22
22
  /** Minimum pixels per second of audio (zoom) */
23
23
  minPxPerSec?: number;
24
+ /** Stretch the waveform to fill the container, true by default */
25
+ fillParent?: boolean;
24
26
  /** Audio URL */
25
27
  url?: string;
26
28
  /** Pre-computed audio data */
27
- channelData?: Float32Array[];
29
+ peaks?: Float32Array[];
28
30
  /** Pre-computed duration */
29
31
  duration?: number;
30
- /** Player "backend", the default is MediaElement */
31
- backend?: PlayerType;
32
32
  /** Use an existing media element instead of creating one */
33
33
  media?: HTMLMediaElement;
34
+ /** Play the audio on load */
35
+ autoplay?: boolean;
36
+ /** Is the waveform clickable? */
37
+ interactive?: boolean;
38
+ /** Hide scrollbar **/
39
+ noScrollbar?: boolean;
40
+ };
41
+ declare const defaultOptions: {
42
+ height: number;
43
+ waveColor: string;
44
+ progressColor: string;
45
+ cursorWidth: number;
46
+ minPxPerSec: number;
47
+ fillParent: boolean;
48
+ interactive: boolean;
34
49
  };
35
50
  export type WaveSurferEvents = {
51
+ decode: {
52
+ duration: number;
53
+ };
54
+ canplay: {
55
+ duration: number;
56
+ };
36
57
  ready: {
37
58
  duration: number;
38
59
  };
39
- canplay: void;
40
60
  play: void;
41
61
  pause: void;
42
- audioprocess: {
62
+ timeupdate: {
63
+ currentTime: number;
64
+ };
65
+ seeking: {
66
+ currentTime: number;
67
+ };
68
+ seekClick: {
43
69
  currentTime: number;
44
70
  };
45
- seek: {
46
- time: number;
71
+ zoom: {
72
+ minPxPerSec: number;
47
73
  };
74
+ destroy: void;
48
75
  };
49
76
  export type WaveSurferPluginParams = {
50
77
  wavesurfer: WaveSurfer;
51
- renderer: WaveSurfer['renderer'];
78
+ container: HTMLElement;
79
+ wrapper: HTMLElement;
52
80
  };
53
- export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
54
- private options;
81
+ declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
82
+ options: WaveSurferOptions & typeof defaultOptions;
55
83
  private fetcher;
56
84
  private decoder;
57
85
  private renderer;
@@ -59,8 +87,7 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
59
87
  private timer;
60
88
  private plugins;
61
89
  private subscriptions;
62
- private channelData;
63
- private duration;
90
+ private decodedData;
64
91
  /** Create a new WaveSurfer instance */
65
92
  static create(options: WaveSurferOptions): WaveSurfer;
66
93
  /** Create a new WaveSurfer instance */
@@ -68,9 +95,8 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
68
95
  private initPlayerEvents;
69
96
  private initRendererEvents;
70
97
  private initTimerEvents;
71
- /** Unmount wavesurfer */
72
- destroy(): void;
73
- /** Load an audio file by URL */
98
+ private initReadyEvent;
99
+ /** Load an audio file by URL, with optional pre-decoded audio data */
74
100
  load(url: string, channelData?: Float32Array[], duration?: number): Promise<void>;
75
101
  /** Zoom in or out */
76
102
  zoom(minPxPerSec: number): void;
@@ -86,7 +112,27 @@ export declare class WaveSurfer extends EventEmitter<WaveSurferEvents> {
86
112
  getDuration(): number;
87
113
  /** Get the current audio position in seconds */
88
114
  getCurrentTime(): number;
115
+ /** Get the audio volume */
116
+ getVolume(): number;
117
+ /** Set the audio volume */
118
+ setVolume(volume: number): void;
119
+ /** Get the audio muted state */
120
+ getMuted(): boolean;
121
+ /** Mute or unmute the audio */
122
+ setMuted(muted: boolean): void;
123
+ /** Get playback rate */
124
+ getPlaybackRate(): number;
125
+ /** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
126
+ setPlaybackRate(rate: number, preservePitch?: boolean): void;
89
127
  /** Register and initialize a plugin */
90
- registerPlugin<T extends BasePlugin<GeneralEventTypes>>(CustomPlugin: new (params: WaveSurferPluginParams) => T): T;
128
+ registerPlugin<T extends BasePlugin<GeneralEventTypes, Options>, Options>(CustomPlugin: new (params: WaveSurferPluginParams, options: Options) => T, options: Options): T;
129
+ /** Get the decoded audio data */
130
+ getDecodedData(): AudioBuffer | null;
131
+ /** Get the raw media element */
132
+ getMediaElement(): HTMLMediaElement | null;
133
+ /** Toggle if the waveform should react to clicks */
134
+ toggleInteractive(isInteractive: boolean): void;
135
+ /** Unmount wavesurfer */
136
+ destroy(): void;
91
137
  }
92
138
  export default WaveSurfer;
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import Fetcher from './fetcher.js';
2
+ import Decoder from './decoder.js';
3
+ import Renderer from './renderer.js';
4
+ import Player from './player.js';
5
+ import EventEmitter from './event-emitter.js';
6
+ import Timer from './timer.js';
7
+ const defaultOptions = {
8
+ height: 128,
9
+ waveColor: '#999',
10
+ progressColor: '#555',
11
+ cursorWidth: 1,
12
+ minPxPerSec: 0,
13
+ fillParent: true,
14
+ interactive: true,
15
+ };
16
+ class WaveSurfer extends EventEmitter {
17
+ options;
18
+ fetcher;
19
+ decoder;
20
+ renderer;
21
+ player;
22
+ timer;
23
+ plugins = [];
24
+ subscriptions = [];
25
+ decodedData = null;
26
+ /** Create a new WaveSurfer instance */
27
+ static create(options) {
28
+ return new WaveSurfer(options);
29
+ }
30
+ /** Create a new WaveSurfer instance */
31
+ constructor(options) {
32
+ super();
33
+ this.options = Object.assign({}, defaultOptions, options);
34
+ this.fetcher = new Fetcher();
35
+ this.decoder = new Decoder();
36
+ this.timer = new Timer();
37
+ this.player = new Player({
38
+ media: this.options.media,
39
+ autoplay: this.options.autoplay,
40
+ });
41
+ this.renderer = new Renderer({
42
+ container: this.options.container,
43
+ height: this.options.height,
44
+ waveColor: this.options.waveColor,
45
+ progressColor: this.options.progressColor,
46
+ cursorColor: this.options.cursorColor,
47
+ cursorWidth: this.options.cursorWidth,
48
+ minPxPerSec: this.options.minPxPerSec,
49
+ fillParent: this.options.fillParent,
50
+ barWidth: this.options.barWidth,
51
+ barGap: this.options.barGap,
52
+ barRadius: this.options.barRadius,
53
+ noScrollbar: this.options.noScrollbar,
54
+ });
55
+ this.initPlayerEvents();
56
+ this.initRendererEvents();
57
+ this.initTimerEvents();
58
+ this.initReadyEvent();
59
+ const url = this.options.url || this.options.media?.src;
60
+ if (url) {
61
+ this.load(url, this.options.peaks, this.options.duration);
62
+ }
63
+ }
64
+ initPlayerEvents() {
65
+ this.subscriptions.push(this.player.on('timeupdate', () => {
66
+ const currentTime = this.getCurrentTime();
67
+ this.renderer.renderProgress(currentTime / this.getDuration(), this.isPlaying());
68
+ this.emit('timeupdate', { currentTime });
69
+ }), this.player.on('play', () => {
70
+ this.emit('play');
71
+ this.timer.start();
72
+ }), this.player.on('pause', () => {
73
+ this.emit('pause');
74
+ this.timer.stop();
75
+ }), this.player.on('canplay', () => {
76
+ this.emit('canplay', { duration: this.getDuration() });
77
+ }), this.player.on('seeking', () => {
78
+ this.emit('seeking', { currentTime: this.getCurrentTime() });
79
+ }));
80
+ }
81
+ initRendererEvents() {
82
+ // Seek on click
83
+ this.subscriptions.push(this.renderer.on('click', ({ relativeX }) => {
84
+ if (this.options.interactive) {
85
+ const time = this.getDuration() * relativeX;
86
+ this.seekTo(time);
87
+ this.emit('seekClick', { currentTime: this.getCurrentTime() });
88
+ }
89
+ }));
90
+ }
91
+ initTimerEvents() {
92
+ // The timer fires every 16ms for a smooth progress animation
93
+ this.subscriptions.push(this.timer.on('tick', () => {
94
+ const currentTime = this.getCurrentTime();
95
+ this.renderer.renderProgress(currentTime / this.getDuration(), true);
96
+ this.emit('timeupdate', { currentTime });
97
+ }));
98
+ }
99
+ initReadyEvent() {
100
+ let isDecoded = false;
101
+ let isPlayable = false;
102
+ const emitReady = () => {
103
+ if (isDecoded && isPlayable) {
104
+ this.emit('ready', { duration: this.getDuration() });
105
+ }
106
+ };
107
+ this.subscriptions.push(this.on('decode', () => {
108
+ isDecoded = true;
109
+ emitReady();
110
+ }), this.on('canplay', () => {
111
+ isPlayable = true;
112
+ emitReady();
113
+ }), this.player.on('waiting', () => {
114
+ isPlayable = false;
115
+ isDecoded = false;
116
+ }));
117
+ }
118
+ /** Load an audio file by URL, with optional pre-decoded audio data */
119
+ async load(url, channelData, duration) {
120
+ this.player.loadUrl(url);
121
+ // Fetch and decode the audio of no pre-computed audio data is provided
122
+ if (channelData == null) {
123
+ const audio = await this.fetcher.load(url);
124
+ const data = await this.decoder.decode(audio);
125
+ this.decodedData = data;
126
+ }
127
+ else {
128
+ if (!duration) {
129
+ duration =
130
+ (await new Promise((resolve) => {
131
+ this.player.on('canplay', () => resolve(this.getDuration()), {
132
+ once: true,
133
+ });
134
+ })) || 0;
135
+ }
136
+ this.decodedData = {
137
+ duration,
138
+ numberOfChannels: channelData.length,
139
+ sampleRate: channelData[0].length / duration,
140
+ getChannelData: (i) => channelData[i],
141
+ };
142
+ }
143
+ this.renderer.render(this.decodedData);
144
+ this.emit('decode', { duration: this.decodedData.duration });
145
+ }
146
+ /** Zoom in or out */
147
+ zoom(minPxPerSec) {
148
+ if (!this.decodedData) {
149
+ throw new Error('No audio loaded');
150
+ }
151
+ this.renderer.zoom(this.decodedData, minPxPerSec);
152
+ this.emit('zoom', { minPxPerSec });
153
+ }
154
+ /** Start playing the audio */
155
+ play() {
156
+ this.player.play();
157
+ }
158
+ /** Pause the audio */
159
+ pause() {
160
+ this.player.pause();
161
+ }
162
+ /** Skip to a time position in seconds */
163
+ seekTo(time) {
164
+ this.player.seekTo(time);
165
+ }
166
+ /** Check if the audio is playing */
167
+ isPlaying() {
168
+ return this.player.isPlaying();
169
+ }
170
+ /** Get the duration of the audio in seconds */
171
+ getDuration() {
172
+ return this.player.getDuration() || this.decodedData?.duration || 0;
173
+ }
174
+ /** Get the current audio position in seconds */
175
+ getCurrentTime() {
176
+ return this.player.getCurrentTime();
177
+ }
178
+ /** Get the audio volume */
179
+ getVolume() {
180
+ return this.player.getVolume();
181
+ }
182
+ /** Set the audio volume */
183
+ setVolume(volume) {
184
+ this.player.setVolume(volume);
185
+ }
186
+ /** Get the audio muted state */
187
+ getMuted() {
188
+ return this.player.getMuted();
189
+ }
190
+ /** Mute or unmute the audio */
191
+ setMuted(muted) {
192
+ this.player.setMuted(muted);
193
+ }
194
+ /** Get playback rate */
195
+ getPlaybackRate() {
196
+ return this.player.getPlaybackRate();
197
+ }
198
+ /** Set playback rate, with an optional parameter to NOT preserve the pitch if false */
199
+ setPlaybackRate(rate, preservePitch) {
200
+ this.player.setPlaybackRate(rate, preservePitch);
201
+ }
202
+ /** Register and initialize a plugin */
203
+ registerPlugin(CustomPlugin, options) {
204
+ const plugin = new CustomPlugin({
205
+ wavesurfer: this,
206
+ container: this.renderer.getContainer(),
207
+ wrapper: this.renderer.getWrapper(),
208
+ }, options);
209
+ this.plugins.push(plugin);
210
+ plugin.once('destroy', () => {
211
+ this.plugins = this.plugins.filter((p) => p !== plugin);
212
+ });
213
+ return plugin;
214
+ }
215
+ /** Get the decoded audio data */
216
+ getDecodedData() {
217
+ return this.decodedData;
218
+ }
219
+ /** Get the raw media element */
220
+ getMediaElement() {
221
+ return this.player.getMediaElement();
222
+ }
223
+ /** Toggle if the waveform should react to clicks */
224
+ toggleInteractive(isInteractive) {
225
+ this.options.interactive = isInteractive;
226
+ }
227
+ /** Unmount wavesurfer */
228
+ destroy() {
229
+ this.emit('destroy');
230
+ this.subscriptions.forEach((unsubscribe) => unsubscribe());
231
+ this.plugins.forEach((plugin) => plugin.destroy());
232
+ this.timer.destroy();
233
+ this.player.destroy();
234
+ this.decoder.destroy();
235
+ this.renderer.destroy();
236
+ }
237
+ }
238
+ export default WaveSurfer;
package/dist/player.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  declare class Player {
2
2
  protected media: HTMLMediaElement;
3
3
  private isExternalMedia;
4
- constructor({ media }: {
4
+ private hasPlayedOnce;
5
+ private subscriptions;
6
+ constructor({ media, autoplay }: {
5
7
  media?: HTMLMediaElement;
8
+ autoplay?: boolean;
6
9
  });
7
- on(event: keyof HTMLMediaElementEventMap, callback: () => void): () => void;
10
+ on(event: keyof HTMLMediaElementEventMap, callback: () => void, options?: AddEventListenerOptions): () => void;
8
11
  destroy(): void;
9
12
  loadUrl(src: string): void;
10
13
  getCurrentTime(): number;
@@ -12,5 +15,13 @@ declare class Player {
12
15
  pause(): void;
13
16
  isPlaying(): boolean;
14
17
  seekTo(time: number): void;
18
+ getDuration(): number;
19
+ getVolume(): number;
20
+ setVolume(volume: number): void;
21
+ getMuted(): boolean;
22
+ setMuted(muted: boolean): void;
23
+ getPlaybackRate(): number;
24
+ setPlaybackRate(rate: number, preservePitch?: boolean): void;
25
+ getMediaElement(): HTMLMediaElement;
15
26
  }
16
27
  export default Player;
package/dist/player.js ADDED
@@ -0,0 +1,90 @@
1
+ class Player {
2
+ media;
3
+ isExternalMedia = false;
4
+ hasPlayedOnce = false;
5
+ subscriptions = [];
6
+ constructor({ media, autoplay }) {
7
+ if (media) {
8
+ this.media = media;
9
+ this.isExternalMedia = true;
10
+ }
11
+ else {
12
+ this.media = document.createElement('audio');
13
+ }
14
+ this.subscriptions.push(
15
+ // Track the first play() call
16
+ this.on('play', () => {
17
+ this.hasPlayedOnce = true;
18
+ }, { once: true }));
19
+ // Autoplay
20
+ if (autoplay) {
21
+ this.media.autoplay = true;
22
+ }
23
+ }
24
+ on(event, callback, options) {
25
+ this.media.addEventListener(event, callback, options);
26
+ return () => this.media.removeEventListener(event, callback);
27
+ }
28
+ destroy() {
29
+ this.subscriptions.forEach((unsubscribe) => {
30
+ unsubscribe();
31
+ });
32
+ this.media.pause();
33
+ if (!this.isExternalMedia) {
34
+ this.media.remove();
35
+ }
36
+ }
37
+ loadUrl(src) {
38
+ this.media.src = src;
39
+ }
40
+ getCurrentTime() {
41
+ return this.media.currentTime;
42
+ }
43
+ play() {
44
+ this.media.play();
45
+ }
46
+ pause() {
47
+ this.media.pause();
48
+ }
49
+ isPlaying() {
50
+ return this.media.currentTime > 0 && !this.media.paused && !this.media.ended;
51
+ }
52
+ seekTo(time) {
53
+ // iOS Safari requires a play() call before seeking
54
+ if (!this.hasPlayedOnce && navigator.userAgent.match(/(iPad|iPhone|iPod)/g)) {
55
+ this.media.play()?.then?.(() => {
56
+ setTimeout(() => this.media.pause(), 10);
57
+ });
58
+ }
59
+ this.media.currentTime = time;
60
+ }
61
+ getDuration() {
62
+ return this.media.duration;
63
+ }
64
+ getVolume() {
65
+ return this.media.volume;
66
+ }
67
+ setVolume(volume) {
68
+ this.media.volume = volume;
69
+ }
70
+ getMuted() {
71
+ return this.media.muted;
72
+ }
73
+ setMuted(muted) {
74
+ this.media.muted = muted;
75
+ }
76
+ getPlaybackRate() {
77
+ return this.media.playbackRate;
78
+ }
79
+ setPlaybackRate(rate, preservePitch) {
80
+ // preservePitch is true by default in most browsers
81
+ if (preservePitch != null) {
82
+ this.media.preservesPitch = preservePitch;
83
+ }
84
+ this.media.playbackRate = rate;
85
+ }
86
+ getMediaElement() {
87
+ return this.media;
88
+ }
89
+ }
90
+ export default Player;