@arraypress/waveform-player 1.5.1 → 1.6.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 +83 -18
- package/dist/waveform-player.esm.js +2 -2
- package/dist/waveform-player.js +195 -41
- package/dist/waveform-player.min.js +2 -2
- package/package.json +1 -1
- package/src/js/core.js +243 -52
- package/src/js/themes.js +7 -0
package/src/js/core.js
CHANGED
|
@@ -253,8 +253,18 @@ export class WaveformPlayer {
|
|
|
253
253
|
/**
|
|
254
254
|
* Create audio element
|
|
255
255
|
* @private
|
|
256
|
+
*
|
|
257
|
+
* No-op in `audioMode: 'external'` — the player has no audio of its
|
|
258
|
+
* own; an external controller (e.g. WaveformBar) owns playback and
|
|
259
|
+
* pushes state in via setPlayingState() / setProgress(). The
|
|
260
|
+
* `this.audio` field stays null in that mode; downstream code must
|
|
261
|
+
* null-check it.
|
|
256
262
|
*/
|
|
257
263
|
createAudio() {
|
|
264
|
+
if (this.options.audioMode === 'external') {
|
|
265
|
+
this.audio = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
258
268
|
this.audio = new Audio();
|
|
259
269
|
this.audio.preload = this.options.preload || 'metadata';
|
|
260
270
|
this.audio.crossOrigin = 'anonymous';
|
|
@@ -269,8 +279,12 @@ export class WaveformPlayer {
|
|
|
269
279
|
* @private
|
|
270
280
|
*/
|
|
271
281
|
initPlaybackSpeed() {
|
|
272
|
-
//
|
|
273
|
-
|
|
282
|
+
// External mode has no <audio> element, so the speed control
|
|
283
|
+
// doesn't apply locally — the external controller (e.g.
|
|
284
|
+
// WaveformBar) owns playback rate. Skip the audio init but
|
|
285
|
+
// still bind the speed control UI in case the controller
|
|
286
|
+
// wants to mirror rate changes via events later.
|
|
287
|
+
if (this.audio && this.options.playbackRate && this.options.playbackRate !== 1) {
|
|
274
288
|
this.audio.playbackRate = this.options.playbackRate;
|
|
275
289
|
}
|
|
276
290
|
|
|
@@ -336,30 +350,37 @@ export class WaveformPlayer {
|
|
|
336
350
|
this.container.focus();
|
|
337
351
|
});
|
|
338
352
|
|
|
339
|
-
// Keyboard events
|
|
353
|
+
// Keyboard events. In external mode `this.audio` is null, so
|
|
354
|
+
// seek/volume/mute keys are no-ops (the external controller
|
|
355
|
+
// owns those). Space (togglePlay) still works because togglePlay
|
|
356
|
+
// routes through the request-play/pause events.
|
|
340
357
|
this.container.addEventListener('keydown', (e) => {
|
|
341
358
|
if (document.activeElement !== this.container) return;
|
|
342
359
|
|
|
343
360
|
const key = e.key;
|
|
344
|
-
const
|
|
361
|
+
const hasAudio = !!this.audio;
|
|
362
|
+
const currentTime = hasAudio ? this.audio.currentTime : 0;
|
|
345
363
|
|
|
346
364
|
// Handle number keys 0-9 for seeking
|
|
347
|
-
if (key >= '0' && key <= '9') {
|
|
365
|
+
if (hasAudio && key >= '0' && key <= '9') {
|
|
348
366
|
e.preventDefault();
|
|
349
367
|
this.seekToPercent(parseInt(key) / 10);
|
|
350
368
|
return;
|
|
351
369
|
}
|
|
352
370
|
|
|
353
|
-
// Handle other keys
|
|
371
|
+
// Handle other keys. Space always works (dispatches
|
|
372
|
+
// request-play in external mode); audio-bound keys only
|
|
373
|
+
// when we own the <audio> element.
|
|
354
374
|
const actions = {
|
|
355
375
|
' ': () => this.togglePlay(),
|
|
356
|
-
'ArrowLeft': () => this.seekTo(Math.max(0, currentTime - 5)),
|
|
357
|
-
'ArrowRight': () => this.seekTo(Math.min(this.audio.duration, currentTime + 5)),
|
|
358
|
-
'ArrowUp': () => this.setVolume(Math.min(1, this.audio.volume + 0.1)),
|
|
359
|
-
'ArrowDown': () => this.setVolume(Math.max(0, this.audio.volume - 0.1)),
|
|
360
|
-
'm': () => this.audio.muted = !this.audio.muted,
|
|
361
|
-
'M': () => this.audio.muted = !this.audio.muted
|
|
362
376
|
};
|
|
377
|
+
if (hasAudio) {
|
|
378
|
+
actions['ArrowLeft'] = () => this.seekTo(Math.max(0, currentTime - 5));
|
|
379
|
+
actions['ArrowRight'] = () => this.seekTo(Math.min(this.audio.duration, currentTime + 5));
|
|
380
|
+
actions['ArrowUp'] = () => this.setVolume(Math.min(1, this.audio.volume + 0.1));
|
|
381
|
+
actions['ArrowDown'] = () => this.setVolume(Math.max(0, this.audio.volume - 0.1));
|
|
382
|
+
actions['m'] = actions['M'] = () => this.audio.muted = !this.audio.muted;
|
|
383
|
+
}
|
|
363
384
|
|
|
364
385
|
if (actions[key]) {
|
|
365
386
|
e.preventDefault();
|
|
@@ -374,6 +395,10 @@ export class WaveformPlayer {
|
|
|
374
395
|
*/
|
|
375
396
|
initMediaSession() {
|
|
376
397
|
if (!('mediaSession' in navigator) || !this.options.enableMediaSession) return;
|
|
398
|
+
// Skip Media Session in external mode — the controller (e.g.
|
|
399
|
+
// WaveformBar) owns audio playback and registers its own Media
|
|
400
|
+
// Session handlers; ours would conflict with its.
|
|
401
|
+
if (!this.audio) return;
|
|
377
402
|
|
|
378
403
|
// Set metadata
|
|
379
404
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
@@ -410,21 +435,30 @@ export class WaveformPlayer {
|
|
|
410
435
|
* @private
|
|
411
436
|
*/
|
|
412
437
|
bindEvents() {
|
|
413
|
-
// Play button (only if controls are shown)
|
|
438
|
+
// Play button (only if controls are shown). In external mode
|
|
439
|
+
// togglePlay() dispatches the request-play/pause events so the
|
|
440
|
+
// controller can decide what to do; the click still goes through
|
|
441
|
+
// here.
|
|
414
442
|
if (this.playBtn) {
|
|
415
443
|
this.playBtn.addEventListener('click', () => this.togglePlay());
|
|
416
444
|
}
|
|
417
445
|
|
|
418
|
-
// Audio events
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.audio
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
446
|
+
// Audio events — only when we own an <audio> element. External
|
|
447
|
+
// mode receives state via setPlayingState() / setProgress() from
|
|
448
|
+
// the controller, so we have nothing to listen to here.
|
|
449
|
+
if (this.audio) {
|
|
450
|
+
this.audio.addEventListener('loadstart', () => this.setLoading(true));
|
|
451
|
+
this.audio.addEventListener('loadedmetadata', () => this.onMetadataLoaded());
|
|
452
|
+
this.audio.addEventListener('canplay', () => this.setLoading(false));
|
|
453
|
+
this.audio.addEventListener('play', () => this.onPlay());
|
|
454
|
+
this.audio.addEventListener('pause', () => this.onPause());
|
|
455
|
+
this.audio.addEventListener('ended', () => this.onEnded());
|
|
456
|
+
this.audio.addEventListener('error', (e) => this.onError(e));
|
|
457
|
+
}
|
|
426
458
|
|
|
427
|
-
// Canvas interactions
|
|
459
|
+
// Canvas interactions — seek-on-click. In external mode the
|
|
460
|
+
// canvas click dispatches a `waveformplayer:request-seek` event
|
|
461
|
+
// so the controller can position its own audio element.
|
|
428
462
|
this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));
|
|
429
463
|
|
|
430
464
|
// Window resize - store handler for cleanup
|
|
@@ -463,24 +497,31 @@ export class WaveformPlayer {
|
|
|
463
497
|
this.progress = 0;
|
|
464
498
|
this.hasError = false;
|
|
465
499
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
500
|
+
// In external mode we don't own an <audio> element — skip
|
|
501
|
+
// src assignment + metadata-wait, but still generate the
|
|
502
|
+
// waveform peaks so the canvas can render the visualization.
|
|
503
|
+
// Duration / current time come from the external controller
|
|
504
|
+
// via setProgress().
|
|
505
|
+
if (this.audio) {
|
|
506
|
+
// Set audio source
|
|
507
|
+
this.audio.src = url;
|
|
508
|
+
|
|
509
|
+
// Wait for metadata to load
|
|
510
|
+
await new Promise((resolve, reject) => {
|
|
511
|
+
const metadataHandler = () => {
|
|
512
|
+
this.audio.removeEventListener('loadedmetadata', metadataHandler);
|
|
513
|
+
this.audio.removeEventListener('error', errorHandler);
|
|
514
|
+
resolve();
|
|
515
|
+
};
|
|
516
|
+
const errorHandler = (e) => {
|
|
517
|
+
this.audio.removeEventListener('loadedmetadata', metadataHandler);
|
|
518
|
+
this.audio.removeEventListener('error', errorHandler);
|
|
519
|
+
reject(e);
|
|
520
|
+
};
|
|
521
|
+
this.audio.addEventListener('loadedmetadata', metadataHandler);
|
|
522
|
+
this.audio.addEventListener('error', errorHandler);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
484
525
|
|
|
485
526
|
// Set title
|
|
486
527
|
const title = this.options.title || extractTitleFromUrl(url);
|
|
@@ -538,9 +579,11 @@ export class WaveformPlayer {
|
|
|
538
579
|
this.pause();
|
|
539
580
|
}
|
|
540
581
|
|
|
541
|
-
// Reset audio element completely
|
|
542
|
-
this.audio
|
|
543
|
-
|
|
582
|
+
// Reset audio element completely (only when we own one)
|
|
583
|
+
if (this.audio) {
|
|
584
|
+
this.audio.src = '';
|
|
585
|
+
this.audio.load();
|
|
586
|
+
}
|
|
544
587
|
|
|
545
588
|
// Clear any errors
|
|
546
589
|
this.hasError = false;
|
|
@@ -567,7 +610,7 @@ export class WaveformPlayer {
|
|
|
567
610
|
});
|
|
568
611
|
|
|
569
612
|
// Apply preload setting if it was changed
|
|
570
|
-
if (options.preload) {
|
|
613
|
+
if (options.preload && this.audio) {
|
|
571
614
|
this.audio.preload = options.preload;
|
|
572
615
|
}
|
|
573
616
|
|
|
@@ -605,12 +648,16 @@ export class WaveformPlayer {
|
|
|
605
648
|
* @private
|
|
606
649
|
*/
|
|
607
650
|
setWaveformData(data) {
|
|
608
|
-
// URL to JSON file — fetch peaks
|
|
651
|
+
// URL to JSON file — fetch peaks and maybe markers
|
|
609
652
|
if (typeof data === 'string' && data.trim().endsWith('.json')) {
|
|
610
653
|
fetch(data.trim())
|
|
611
654
|
.then(r => r.json())
|
|
612
655
|
.then(json => {
|
|
613
656
|
this.waveformData = Array.isArray(json) ? json : (json.peaks || []);
|
|
657
|
+
if (json.markers && !this.options.markers?.length) {
|
|
658
|
+
this.options.markers = json.markers;
|
|
659
|
+
this.renderMarkers();
|
|
660
|
+
}
|
|
614
661
|
this.drawWaveform();
|
|
615
662
|
})
|
|
616
663
|
.catch(() => {});
|
|
@@ -727,12 +774,31 @@ export class WaveformPlayer {
|
|
|
727
774
|
* @private
|
|
728
775
|
*/
|
|
729
776
|
handleCanvasClick(event) {
|
|
730
|
-
|
|
731
|
-
|
|
777
|
+
// In external mode the player has no audio of its own —
|
|
778
|
+
// dispatch a cancelable `waveformplayer:request-seek` event
|
|
779
|
+
// with the target percentage so the controller can seek its
|
|
780
|
+
// own audio. Locally we just update the visual progress so
|
|
781
|
+
// the canvas paints the new position immediately (the
|
|
782
|
+
// controller's progress event will reconcile shortly after).
|
|
732
783
|
const rect = this.canvas.getBoundingClientRect();
|
|
733
784
|
const x = event.clientX - rect.left;
|
|
734
785
|
const targetPercent = Math.max(0, Math.min(1, x / rect.width));
|
|
735
786
|
|
|
787
|
+
if (this.options.audioMode === 'external') {
|
|
788
|
+
const evt = new CustomEvent('waveformplayer:request-seek', {
|
|
789
|
+
bubbles: true,
|
|
790
|
+
cancelable: true,
|
|
791
|
+
detail: { ...this._buildTrackDetail(), percent: targetPercent }
|
|
792
|
+
});
|
|
793
|
+
this.container.dispatchEvent(evt);
|
|
794
|
+
if (!evt.defaultPrevented) {
|
|
795
|
+
this.progress = targetPercent;
|
|
796
|
+
this.drawWaveform?.();
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (!this.audio || !this.audio.duration) return;
|
|
736
802
|
this.seekToPercent(targetPercent);
|
|
737
803
|
}
|
|
738
804
|
|
|
@@ -897,7 +963,10 @@ export class WaveformPlayer {
|
|
|
897
963
|
this.stopSmoothUpdate();
|
|
898
964
|
|
|
899
965
|
const update = () => {
|
|
900
|
-
|
|
966
|
+
// In external mode the canvas redraws are driven by
|
|
967
|
+
// setProgress() pushes from the controller — no internal
|
|
968
|
+
// RAF needed. Self-mode keeps the smooth-update loop.
|
|
969
|
+
if (this.isPlaying && this.audio && this.audio.duration) {
|
|
901
970
|
this.updateProgress();
|
|
902
971
|
this.updateTimer = requestAnimationFrame(update);
|
|
903
972
|
}
|
|
@@ -922,7 +991,9 @@ export class WaveformPlayer {
|
|
|
922
991
|
* @private
|
|
923
992
|
*/
|
|
924
993
|
updateProgress() {
|
|
925
|
-
|
|
994
|
+
// Self-mode only — external mode receives progress via
|
|
995
|
+
// setProgress() from the controller and never calls this.
|
|
996
|
+
if (!this.audio || !this.audio.duration) return;
|
|
926
997
|
|
|
927
998
|
const newProgress = this.audio.currentTime / this.audio.duration;
|
|
928
999
|
|
|
@@ -988,8 +1059,20 @@ export class WaveformPlayer {
|
|
|
988
1059
|
// ============================================
|
|
989
1060
|
|
|
990
1061
|
/**
|
|
991
|
-
* Play audio
|
|
992
|
-
*
|
|
1062
|
+
* Play audio.
|
|
1063
|
+
*
|
|
1064
|
+
* In `audioMode: 'self'` (default): calls the underlying <audio>
|
|
1065
|
+
* element's play(). Returns the promise from HTMLMediaElement.play().
|
|
1066
|
+
*
|
|
1067
|
+
* In `audioMode: 'external'`: dispatches a cancelable
|
|
1068
|
+
* `waveformplayer:request-play` event with the track metadata and
|
|
1069
|
+
* does NOT touch any audio element. Returns `undefined`. An external
|
|
1070
|
+
* controller (e.g. WaveformBar) listens for this event and starts
|
|
1071
|
+
* playback on its own audio source, then pushes state back via
|
|
1072
|
+
* setPlayingState() / setProgress(). Calling preventDefault() on
|
|
1073
|
+
* the event lets the controller veto the play (state is unchanged).
|
|
1074
|
+
*
|
|
1075
|
+
* @return {Promise|undefined}
|
|
993
1076
|
*/
|
|
994
1077
|
play() {
|
|
995
1078
|
if (this.options.singlePlay && WaveformPlayer.currentlyPlaying &&
|
|
@@ -997,20 +1080,128 @@ export class WaveformPlayer {
|
|
|
997
1080
|
WaveformPlayer.currentlyPlaying.pause();
|
|
998
1081
|
}
|
|
999
1082
|
|
|
1083
|
+
if (this.options.audioMode === 'external') {
|
|
1084
|
+
const evt = new CustomEvent('waveformplayer:request-play', {
|
|
1085
|
+
bubbles: true,
|
|
1086
|
+
cancelable: true,
|
|
1087
|
+
detail: this._buildTrackDetail()
|
|
1088
|
+
});
|
|
1089
|
+
this.container.dispatchEvent(evt);
|
|
1090
|
+
// If the controller cancels (preventDefault), don't claim
|
|
1091
|
+
// "currentlyPlaying" — the controller didn't accept the play.
|
|
1092
|
+
if (!evt.defaultPrevented) {
|
|
1093
|
+
WaveformPlayer.currentlyPlaying = this;
|
|
1094
|
+
}
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1000
1098
|
WaveformPlayer.currentlyPlaying = this;
|
|
1001
1099
|
return this.audio.play();
|
|
1002
1100
|
}
|
|
1003
1101
|
|
|
1004
1102
|
/**
|
|
1005
|
-
* Pause audio
|
|
1103
|
+
* Pause audio.
|
|
1104
|
+
*
|
|
1105
|
+
* In `audioMode: 'external'`, dispatches `waveformplayer:request-pause`
|
|
1106
|
+
* (cancelable) and does NOT touch any audio element. See play().
|
|
1006
1107
|
*/
|
|
1007
1108
|
pause() {
|
|
1008
1109
|
if (WaveformPlayer.currentlyPlaying === this) {
|
|
1009
1110
|
WaveformPlayer.currentlyPlaying = null;
|
|
1010
1111
|
}
|
|
1112
|
+
if (this.options.audioMode === 'external') {
|
|
1113
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:request-pause', {
|
|
1114
|
+
bubbles: true,
|
|
1115
|
+
cancelable: true,
|
|
1116
|
+
detail: this._buildTrackDetail()
|
|
1117
|
+
}));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1011
1120
|
this.audio.pause();
|
|
1012
1121
|
}
|
|
1013
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Build the track detail object dispatched by request-play /
|
|
1125
|
+
* request-pause events in external audio mode. Mirrors the shape
|
|
1126
|
+
* WaveformBar.play() accepts so a controller can forward it
|
|
1127
|
+
* directly: `WaveformBar.play(event.detail)`.
|
|
1128
|
+
*
|
|
1129
|
+
* @private
|
|
1130
|
+
* @return {{url:string,title:?string,subtitle:?string,artist:?string,artwork:?string,player:WaveformPlayer}}
|
|
1131
|
+
*/
|
|
1132
|
+
_buildTrackDetail() {
|
|
1133
|
+
return {
|
|
1134
|
+
url: this.options.url,
|
|
1135
|
+
title: this.options.title,
|
|
1136
|
+
subtitle: this.options.subtitle,
|
|
1137
|
+
artist: this.options.artist,
|
|
1138
|
+
artwork: this.options.artwork,
|
|
1139
|
+
id: this.id,
|
|
1140
|
+
player: this
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* External-mode state pump: flip the play/pause visual state without
|
|
1146
|
+
* touching audio. Mirrors what onPlay()/onPause() do but skips the
|
|
1147
|
+
* audio-element interactions. Safe to call repeatedly — idempotent.
|
|
1148
|
+
*
|
|
1149
|
+
* @param {boolean} playing
|
|
1150
|
+
*/
|
|
1151
|
+
setPlayingState(playing) {
|
|
1152
|
+
const wasPlaying = this.isPlaying;
|
|
1153
|
+
this.isPlaying = !!playing;
|
|
1154
|
+
if (this.playBtn) {
|
|
1155
|
+
this.playBtn.classList.toggle('playing', this.isPlaying);
|
|
1156
|
+
const playIcon = this.playBtn.querySelector('.waveform-icon-play');
|
|
1157
|
+
const pauseIcon = this.playBtn.querySelector('.waveform-icon-pause');
|
|
1158
|
+
if (playIcon) playIcon.style.display = this.isPlaying ? 'none' : 'flex';
|
|
1159
|
+
if (pauseIcon) pauseIcon.style.display = this.isPlaying ? 'flex' : 'none';
|
|
1160
|
+
}
|
|
1161
|
+
if (this.isPlaying && !wasPlaying) {
|
|
1162
|
+
this.startSmoothUpdate?.();
|
|
1163
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:play', {
|
|
1164
|
+
bubbles: true,
|
|
1165
|
+
detail: {player: this, url: this.options.url}
|
|
1166
|
+
}));
|
|
1167
|
+
if (this.options.onPlay) this.options.onPlay(this);
|
|
1168
|
+
} else if (!this.isPlaying && wasPlaying) {
|
|
1169
|
+
this.stopSmoothUpdate?.();
|
|
1170
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:pause', {
|
|
1171
|
+
bubbles: true,
|
|
1172
|
+
detail: {player: this, url: this.options.url}
|
|
1173
|
+
}));
|
|
1174
|
+
if (this.options.onPause) this.options.onPause(this);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* External-mode state pump: update the visualization's progress
|
|
1180
|
+
* from an external clock (e.g. WaveformBar's audio element's
|
|
1181
|
+
* timeupdate). Drives the canvas redraw + the time displays.
|
|
1182
|
+
*
|
|
1183
|
+
* @param {number} currentTime Current playback position in seconds.
|
|
1184
|
+
* @param {number} duration Total track duration in seconds.
|
|
1185
|
+
*/
|
|
1186
|
+
setProgress(currentTime, duration) {
|
|
1187
|
+
if (!duration || duration <= 0) return;
|
|
1188
|
+
this.progress = Math.max(0, Math.min(1, currentTime / duration));
|
|
1189
|
+
// Mirror the existing display update code so callers don't have
|
|
1190
|
+
// to know which DOM elements live where.
|
|
1191
|
+
if (this.currentTimeEl) this.currentTimeEl.textContent = formatTime(currentTime);
|
|
1192
|
+
if (this.totalTimeEl && (!this.totalTimeEl.dataset._extSet || this._extDuration !== duration)) {
|
|
1193
|
+
this.totalTimeEl.textContent = formatTime(duration);
|
|
1194
|
+
this.totalTimeEl.dataset._extSet = '1';
|
|
1195
|
+
this._extDuration = duration;
|
|
1196
|
+
}
|
|
1197
|
+
this.drawWaveform?.();
|
|
1198
|
+
this.container.dispatchEvent(new CustomEvent('waveformplayer:timeupdate', {
|
|
1199
|
+
bubbles: true,
|
|
1200
|
+
detail: {player: this, currentTime, duration, progress: this.progress}
|
|
1201
|
+
}));
|
|
1202
|
+
if (this.options.onTimeUpdate) this.options.onTimeUpdate(this, currentTime, duration);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1014
1205
|
/**
|
|
1015
1206
|
* Toggle play/pause
|
|
1016
1207
|
*/
|
package/src/js/themes.js
CHANGED
|
@@ -125,6 +125,13 @@ export const DEFAULT_OPTIONS = {
|
|
|
125
125
|
samples: 200,
|
|
126
126
|
preload: 'metadata',
|
|
127
127
|
|
|
128
|
+
// Audio mode — 'self' = player owns the <audio> element (default, current
|
|
129
|
+
// behavior). 'external' = player is a visualization-only surface; no audio
|
|
130
|
+
// element is created, play() dispatches `waveformplayer:request-play`
|
|
131
|
+
// instead of calling audio.play(), and setPlayingState/setProgress are
|
|
132
|
+
// expected to be driven by an external controller (e.g. WaveformBar).
|
|
133
|
+
audioMode: 'self',
|
|
134
|
+
|
|
128
135
|
// Playback
|
|
129
136
|
playbackRate: 1,
|
|
130
137
|
showPlaybackSpeed: false,
|