@arraypress/waveform-bar 1.2.0 → 1.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/src/js/core.js CHANGED
@@ -43,7 +43,6 @@ const DEFAULTS = {
43
43
  waveformColor: null,
44
44
  progressColor: null,
45
45
  markerColor: 'rgba(255, 255, 255, 0.25)',
46
- configPath: null, // Directory for auto-resolved config JSON files (e.g. 'waveforms/')
47
46
  volume: 1,
48
47
  storageKey: 'waveform-bar',
49
48
  actions: null,
@@ -312,6 +311,7 @@ export class WaveformBar {
312
311
  this.isPlaying = true;
313
312
  this._updatePlayButton();
314
313
  this._syncPageState();
314
+ this._pumpExternalPlayState(true);
315
315
  const track = this.getCurrentTrack();
316
316
  this._emit('play', {track});
317
317
  if (this.config.onPlay) this.config.onPlay(track);
@@ -320,6 +320,7 @@ export class WaveformBar {
320
320
  this.isPlaying = false;
321
321
  this._updatePlayButton();
322
322
  this._syncPageState();
323
+ this._pumpExternalPlayState(false);
323
324
  this._saveState();
324
325
  const track = this.getCurrentTrack();
325
326
  this._emit('pause', {track});
@@ -329,6 +330,7 @@ export class WaveformBar {
329
330
  this.isPlaying = false;
330
331
  this._updatePlayButton();
331
332
  this._syncPageState();
333
+ this._pumpExternalPlayState(false);
332
334
 
333
335
  // Reset time display
334
336
  if (this.timeCurrentEl) this.timeCurrentEl.textContent = '0:00';
@@ -359,6 +361,11 @@ export class WaveformBar {
359
361
  if (this.timeCurrentEl) this.timeCurrentEl.textContent = formatTime(currentTime);
360
362
  if (this.timeTotalEl) this.timeTotalEl.textContent = formatTime(duration);
361
363
 
364
+ // Mirror progress into any external-mode WaveformPlayer
365
+ // instances tracking this URL — their canvases scrub in
366
+ // sync with the bar's audio.
367
+ this._pumpExternalProgress(currentTime, duration);
368
+
362
369
  // Save state periodically during playback
363
370
  if (!this._lastSaveTime || currentTime - this._lastSaveTime > 2) {
364
371
  this._lastSaveTime = currentTime;
@@ -405,6 +412,123 @@ export class WaveformBar {
405
412
  if (track) this.addToQueue(track);
406
413
  });
407
414
  });
415
+
416
+ // External-mode WaveformPlayer instances on the page act as
417
+ // visualization surfaces controlled by this bar — see the
418
+ // `audioMode: 'external'` option in @arraypress/waveform-player.
419
+ // Each instance dispatches `waveformplayer:request-play`
420
+ // (cancelable) when its play button is clicked; we route that
421
+ // into this bar so the audio always lives in one place.
422
+ this._attachExternalPlayers();
423
+ }
424
+
425
+ /**
426
+ * Discover external-mode WaveformPlayer instances and listen for
427
+ * their request-play / request-pause / request-seek events. Also
428
+ * builds a url → Set<WaveformPlayer> map used by _syncPageState()
429
+ * and the onTimeUpdate callback to push state into the matching
430
+ * inline visualizations.
431
+ *
432
+ * Idempotent — safe to call repeatedly. Late-mounted players are
433
+ * picked up by the MutationObserver in _observeDOM().
434
+ *
435
+ * @private
436
+ */
437
+ _attachExternalPlayers() {
438
+ // Document-level listeners only bind once.
439
+ if (!this._externalListenersBound) {
440
+ this._externalListenersBound = true;
441
+
442
+ document.addEventListener('waveformplayer:request-play', (e) => {
443
+ const t = e.detail;
444
+ if (!t || !t.url) return;
445
+ e.preventDefault();
446
+ this.play(t);
447
+ });
448
+
449
+ document.addEventListener('waveformplayer:request-pause', (e) => {
450
+ const t = e.detail;
451
+ if (!t || !t.url) return;
452
+ // Only honour pause when this is the currently-playing
453
+ // track — pause requests from other players are noise.
454
+ const current = this.getCurrentTrack();
455
+ if (current && current.url === t.url) {
456
+ e.preventDefault();
457
+ if (this.isPlaying) this.togglePlay();
458
+ }
459
+ });
460
+
461
+ document.addEventListener('waveformplayer:request-seek', (e) => {
462
+ const t = e.detail;
463
+ if (!t || !t.url || typeof t.percent !== 'number') return;
464
+ const current = this.getCurrentTrack();
465
+ if (current && current.url === t.url && this.player && this.player.audio) {
466
+ e.preventDefault();
467
+ this.player.seekToPercent(t.percent);
468
+ }
469
+ });
470
+ }
471
+
472
+ // Rebuild the URL → players map from scratch each pass — cheap
473
+ // (single querySelectorAll + Map insert) and avoids stale entries
474
+ // for players that have been torn down. Late-mounted players
475
+ // come in via the MutationObserver tick.
476
+ this._externalPlayers = new Map();
477
+ const WP = window.WaveformPlayer;
478
+ if (!WP || !WP.instances) return;
479
+ document.querySelectorAll('[data-waveform-player][data-audio-mode="external"]').forEach((el) => {
480
+ const inst = WP.instances.get(el.id);
481
+ if (!inst || !inst.options || !inst.options.url) return;
482
+ const url = inst.options.url;
483
+ if (!this._externalPlayers.has(url)) this._externalPlayers.set(url, new Set());
484
+ this._externalPlayers.get(url).add(inst);
485
+ });
486
+ }
487
+
488
+ /**
489
+ * Push playing-state into every external-mode player whose URL
490
+ * matches the currently playing track. Other URLs get set to
491
+ * false (paused) — covers the case where the bar switched tracks
492
+ * and the previously-current external player should stop showing
493
+ * its play indicator.
494
+ *
495
+ * @private
496
+ * @param {boolean} playing
497
+ */
498
+ _pumpExternalPlayState(playing) {
499
+ if (!this._externalPlayers || this._externalPlayers.size === 0) return;
500
+ const current = this.getCurrentTrack();
501
+ const currentUrl = current ? current.url : null;
502
+ this._externalPlayers.forEach((set, url) => {
503
+ const isCurrent = url === currentUrl;
504
+ set.forEach((player) => {
505
+ if (typeof player.setPlayingState === 'function') {
506
+ player.setPlayingState(isCurrent && playing);
507
+ }
508
+ });
509
+ });
510
+ }
511
+
512
+ /**
513
+ * Push progress (currentTime + duration) into the external-mode
514
+ * player(s) tracking the current URL. Called on every timeupdate
515
+ * tick of the internal player.
516
+ *
517
+ * @private
518
+ * @param {number} currentTime
519
+ * @param {number} duration
520
+ */
521
+ _pumpExternalProgress(currentTime, duration) {
522
+ if (!this._externalPlayers || this._externalPlayers.size === 0) return;
523
+ const current = this.getCurrentTrack();
524
+ if (!current) return;
525
+ const set = this._externalPlayers.get(current.url);
526
+ if (!set) return;
527
+ set.forEach((player) => {
528
+ if (typeof player.setProgress === 'function') {
529
+ player.setProgress(currentTime, duration);
530
+ }
531
+ });
408
532
  }
409
533
 
410
534
  _observeDOM() {
@@ -827,6 +951,12 @@ export class WaveformBar {
827
951
  const track = this.getCurrentTrack();
828
952
  if (!track || !this.player) return;
829
953
 
954
+ // Reset any previously-current external player so its UI flips
955
+ // back to "paused" while the new track loads. The new track's
956
+ // onPlay callback will set the matching external to playing
957
+ // once playback actually starts.
958
+ this._pumpExternalPlayState(false);
959
+
830
960
  this.show();
831
961
  this._updateTrackDisplay(track);
832
962
  this._updateFavoriteUI();
@@ -838,14 +968,6 @@ export class WaveformBar {
838
968
  loadOpts.waveform = track.waveform;
839
969
  }
840
970
 
841
- // Auto-resolve config JSON from configPath
842
- if (this.config.configPath && track.url) {
843
- const audioFile = track.url.split('/').pop().split('?')[0];
844
- const jsonFile = audioFile.replace(/\.[^.]+$/, '.json');
845
- const path = this.config.configPath.replace(/\/?$/, '/');
846
- loadOpts.config = path + jsonFile;
847
- }
848
-
849
971
  // Always pass markers — empty array clears previous track's markers
850
972
  if (track.markers && track.markers.length) {
851
973
  const defaultColor = this.config.markerColor;
@@ -1269,14 +1391,6 @@ export class WaveformBar {
1269
1391
  this.player.options.waveform = track.waveform;
1270
1392
  }
1271
1393
 
1272
- // Auto-resolve config JSON from configPath
1273
- if (this.config.configPath && track.url) {
1274
- const audioFile = track.url.split('/').pop().split('?')[0];
1275
- const jsonFile = audioFile.replace(/\.[^.]+$/, '.json');
1276
- const path = this.config.configPath.replace(/\/?$/, '/');
1277
- this.player.options.config = path + jsonFile;
1278
- }
1279
-
1280
1394
  this.player.options.title = track.title || '';
1281
1395
  this.player.options.subtitle = track.artist || '';
1282
1396
 
@@ -1344,4 +1458,5 @@ export class WaveformBar {
1344
1458
  _restoreFavorites() {
1345
1459
  this._favorites = restoreFavorites(this.config.storageKey);
1346
1460
  }
1461
+
1347
1462
  }