@arraypress/waveform-player 1.0.1 → 1.1.1

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
@@ -88,6 +88,10 @@ export class WaveformPlayer {
88
88
  this.init();
89
89
  }
90
90
 
91
+ // ============================================
92
+ // Initialization
93
+ // ============================================
94
+
91
95
  /**
92
96
  * Initialize the player
93
97
  * @private
@@ -95,6 +99,8 @@ export class WaveformPlayer {
95
99
  init() {
96
100
  this.createDOM();
97
101
  this.createAudio();
102
+ this.initPlaybackSpeed();
103
+ this.initKeyboardControls();
98
104
  this.bindEvents();
99
105
  this.setupResizeObserver();
100
106
 
@@ -124,49 +130,84 @@ export class WaveformPlayer {
124
130
  this.container.innerHTML = '';
125
131
  this.container.className = 'waveform-player';
126
132
 
133
+ // Determine button alignment
134
+ // Determine button alignment
135
+ let buttonAlign = this.options.buttonAlign;
136
+ if (buttonAlign === 'auto') {
137
+ // Auto-align based on waveform style
138
+ const style = this.options.waveformStyle;
139
+ if (style === 'bars') {
140
+ buttonAlign = 'bottom';
141
+ } else {
142
+ buttonAlign = 'center'; // blocks, mirror, line, dots, seekbar all center
143
+ }
144
+ }
145
+
127
146
  // Create HTML structure
128
147
  this.container.innerHTML = `
129
- <div class="waveform-player-inner">
130
- <div class="waveform-body">
131
- <div class="waveform-track">
132
- <button class="waveform-btn" aria-label="Play/Pause" style="
133
- border-color: ${this.options.buttonColor};
134
- color: ${this.options.buttonColor};
135
- ">
136
- <span class="waveform-icon-play">${this.options.playIcon}</span>
137
- <span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
138
- </button>
139
-
140
- <div class="waveform-container">
141
- <canvas></canvas>
142
- <div class="waveform-loading" style="display:none;"></div>
143
- <div class="waveform-error" style="display:none;">
144
- <span class="waveform-error-text">Unable to load audio</span>
145
- </div>
146
- </div>
148
+ <div class="waveform-player-inner">
149
+ <div class="waveform-body">
150
+ <div class="waveform-track waveform-align-${buttonAlign}">
151
+ <button class="waveform-btn" aria-label="Play/Pause" style="
152
+ border-color: ${this.options.buttonColor};
153
+ color: ${this.options.buttonColor};
154
+ ">
155
+ <span class="waveform-icon-play">${this.options.playIcon}</span>
156
+ <span class="waveform-icon-pause" style="display:none;">${this.options.pauseIcon}</span>
157
+ </button>
158
+
159
+ <div class="waveform-container">
160
+ <canvas></canvas>
161
+ <div class="waveform-markers"></div>
162
+ <div class="waveform-loading" style="display:none;"></div>
163
+ <div class="waveform-error" style="display:none;">
164
+ <span class="waveform-error-text">Unable to load audio</span>
147
165
  </div>
148
-
149
- <div class="waveform-info">
150
- <div class="waveform-text">
151
- <span class="waveform-title" style="color: ${this.options.textColor};"></span>
152
- ${this.options.subtitle ? `<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>` : ''}
153
- </div>
154
- <div style="display: flex; align-items: center; gap: 1rem;">
155
- ${this.options.showBPM ? `
156
- <span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
157
- <span class="bpm-value">--</span> BPM
158
- </span>
159
- ` : ''}
160
- ${this.options.showTime ? `
161
- <span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
162
- <span class="time-current">0:00</span> / <span class="time-total">0:00</span>
163
- </span>
164
- ` : ''}
166
+ </div>
167
+ </div>
168
+
169
+ <div class="waveform-info">
170
+ ${this.options.artwork ? `
171
+ <img class="waveform-artwork" src="${this.options.artwork}" alt="Album artwork" style="
172
+ width: 40px;
173
+ height: 40px;
174
+ border-radius: 4px;
175
+ object-fit: cover;
176
+ flex-shrink: 0;
177
+ ">
178
+ ` : ''}
179
+ <div class="waveform-text">
180
+ <span class="waveform-title" style="color: ${this.options.textColor};"></span>
181
+ ${this.options.subtitle ? `<span class="waveform-subtitle" style="color: ${this.options.textSecondaryColor};">${this.options.subtitle}</span>` : ''}
182
+ </div>
183
+ <div style="display: flex; align-items: center; gap: 1rem;">
184
+ ${this.options.showBPM ? `
185
+ <span class="waveform-bpm" style="color: ${this.options.textSecondaryColor}; display: none;">
186
+ <span class="bpm-value">--</span> BPM
187
+ </span>
188
+ ` : ''}
189
+ ${this.options.showPlaybackSpeed ? `
190
+ <div class="waveform-speed">
191
+ <button class="speed-btn" aria-label="Playback speed">
192
+ <span class="speed-value">1x</span>
193
+ </button>
194
+ <div class="speed-menu" style="display: none;">
195
+ ${this.options.playbackRates.map(rate =>
196
+ `<button class="speed-option" data-rate="${rate}">${rate}x</button>`
197
+ ).join('')}
198
+ </div>
165
199
  </div>
166
- </div>
200
+ ` : ''}
201
+ ${this.options.showTime ? `
202
+ <span class="waveform-time" style="color: ${this.options.textSecondaryColor};">
203
+ <span class="time-current">0:00</span> / <span class="time-total">0:00</span>
204
+ </span>
205
+ ` : ''}
167
206
  </div>
168
207
  </div>
169
- `;
208
+ </div>
209
+ </div>
210
+ `;
170
211
 
171
212
  // Get references
172
213
  this.playBtn = this.container.querySelector('.waveform-btn');
@@ -174,12 +215,16 @@ export class WaveformPlayer {
174
215
  this.ctx = this.canvas.getContext('2d');
175
216
  this.titleEl = this.container.querySelector('.waveform-title');
176
217
  this.subtitleEl = this.container.querySelector('.waveform-subtitle');
218
+ this.artworkEl = this.container.querySelector('.waveform-artwork');
177
219
  this.currentTimeEl = this.container.querySelector('.time-current');
178
220
  this.totalTimeEl = this.container.querySelector('.time-total');
179
221
  this.bpmEl = this.container.querySelector('.waveform-bpm');
180
222
  this.bpmValueEl = this.container.querySelector('.bpm-value');
181
223
  this.loadingEl = this.container.querySelector('.waveform-loading');
182
224
  this.errorEl = this.container.querySelector('.waveform-error');
225
+ this.markersContainer = this.container.querySelector('.waveform-markers');
226
+ this.speedBtn = this.container.querySelector('.speed-btn');
227
+ this.speedMenu = this.container.querySelector('.speed-menu');
183
228
 
184
229
  // Set canvas size
185
230
  this.resizeCanvas();
@@ -191,10 +236,155 @@ export class WaveformPlayer {
191
236
  */
192
237
  createAudio() {
193
238
  this.audio = new Audio();
194
- this.audio.preload = 'metadata';
239
+ this.audio.preload = this.options.preload || 'metadata';
195
240
  this.audio.crossOrigin = 'anonymous';
196
241
  }
197
242
 
243
+ // ============================================
244
+ // Feature Initialization
245
+ // ============================================
246
+
247
+ /**
248
+ * Initialize playback speed controls
249
+ * @private
250
+ */
251
+ initPlaybackSpeed() {
252
+ // Set initial playback rate if specified
253
+ if (this.options.playbackRate && this.options.playbackRate !== 1) {
254
+ this.audio.playbackRate = this.options.playbackRate;
255
+ }
256
+
257
+ // Initialize speed control UI if enabled
258
+ if (this.options.showPlaybackSpeed) {
259
+ this.initSpeedControls();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Initialize speed control UI
265
+ * @private
266
+ */
267
+ initSpeedControls() {
268
+ const speedBtn = this.container.querySelector('.speed-btn');
269
+ const speedMenu = this.container.querySelector('.speed-menu');
270
+
271
+ if (!speedBtn || !speedMenu) return;
272
+
273
+ // Toggle menu
274
+ speedBtn.addEventListener('click', (e) => {
275
+ e.stopPropagation();
276
+ speedMenu.style.display = speedMenu.style.display === 'none' ? 'block' : 'none';
277
+ });
278
+
279
+ // Close menu when clicking outside
280
+ document.addEventListener('click', () => {
281
+ speedMenu.style.display = 'none';
282
+ });
283
+
284
+ // Handle speed selection
285
+ speedMenu.addEventListener('click', (e) => {
286
+ e.stopPropagation();
287
+ if (e.target.classList.contains('speed-option')) {
288
+ const rate = parseFloat(e.target.dataset.rate);
289
+ this.setPlaybackRate(rate);
290
+ speedMenu.style.display = 'none';
291
+ }
292
+ });
293
+
294
+ // Set initial UI state
295
+ this.updateSpeedUI();
296
+ }
297
+
298
+ /**
299
+ * Initialize keyboard controls
300
+ * @private
301
+ */
302
+ initKeyboardControls() {
303
+ // Make container focusable but not in tab order by default
304
+ this.container.setAttribute('tabindex', '-1');
305
+
306
+ // Only activate keyboard controls when explicitly focused (clicked)
307
+ this.container.addEventListener('click', () => {
308
+ // Remove focus from all other players
309
+ WaveformPlayer.getAllInstances().forEach(player => {
310
+ if (player !== this) {
311
+ player.container.setAttribute('tabindex', '-1');
312
+ }
313
+ });
314
+ // Make this one focusable
315
+ this.container.setAttribute('tabindex', '0');
316
+ this.container.focus();
317
+ });
318
+
319
+ // Keyboard events
320
+ this.container.addEventListener('keydown', (e) => {
321
+ if (document.activeElement !== this.container) return;
322
+
323
+ const key = e.key;
324
+ const currentTime = this.audio.currentTime;
325
+
326
+ // Handle number keys 0-9 for seeking
327
+ if (key >= '0' && key <= '9') {
328
+ e.preventDefault();
329
+ this.seekToPercent(parseInt(key) / 10);
330
+ return;
331
+ }
332
+
333
+ // Handle other keys
334
+ const actions = {
335
+ ' ': () => this.togglePlay(),
336
+ 'ArrowLeft': () => this.seekTo(Math.max(0, currentTime - 5)),
337
+ 'ArrowRight': () => this.seekTo(Math.min(this.audio.duration, currentTime + 5)),
338
+ 'ArrowUp': () => this.setVolume(Math.min(1, this.audio.volume + 0.1)),
339
+ 'ArrowDown': () => this.setVolume(Math.max(0, this.audio.volume - 0.1)),
340
+ 'm': () => this.audio.muted = !this.audio.muted,
341
+ 'M': () => this.audio.muted = !this.audio.muted
342
+ };
343
+
344
+ if (actions[key]) {
345
+ e.preventDefault();
346
+ actions[key]();
347
+ }
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Initialize Media Session API for system media controls
353
+ * @private
354
+ */
355
+ initMediaSession() {
356
+ if (!('mediaSession' in navigator) || !this.options.enableMediaSession) return;
357
+
358
+ // Set metadata
359
+ navigator.mediaSession.metadata = new MediaMetadata({
360
+ title: this.options.title || 'Unknown Track',
361
+ artist: this.options.subtitle || '',
362
+ album: this.options.album || '',
363
+ artwork: this.options.artwork ? [
364
+ {src: this.options.artwork, sizes: '512x512', type: 'image/jpeg'}
365
+ ] : []
366
+ });
367
+
368
+ // Set up action handlers
369
+ navigator.mediaSession.setActionHandler('play', () => this.play());
370
+ navigator.mediaSession.setActionHandler('pause', () => this.pause());
371
+ navigator.mediaSession.setActionHandler('seekbackward', () => {
372
+ this.seekTo(Math.max(0, this.audio.currentTime - 10));
373
+ });
374
+ navigator.mediaSession.setActionHandler('seekforward', () => {
375
+ this.seekTo(Math.min(this.audio.duration, this.audio.currentTime + 10));
376
+ });
377
+ navigator.mediaSession.setActionHandler('seekto', (details) => {
378
+ if (details.seekTime !== null) {
379
+ this.seekTo(details.seekTime);
380
+ }
381
+ });
382
+ }
383
+
384
+ // ============================================
385
+ // Event Binding
386
+ // ============================================
387
+
198
388
  /**
199
389
  * Bind event listeners
200
390
  * @private
@@ -235,6 +425,10 @@ export class WaveformPlayer {
235
425
  }
236
426
  }
237
427
 
428
+ // ============================================
429
+ // Audio Loading
430
+ // ============================================
431
+
238
432
  /**
239
433
  * Load audio file
240
434
  * @param {string} url - Audio URL
@@ -292,6 +486,8 @@ export class WaveformPlayer {
292
486
  }
293
487
 
294
488
  this.drawWaveform();
489
+ this.renderMarkers();
490
+ this.initMediaSession();
295
491
 
296
492
  // Fire callback
297
493
  if (this.options.onLoad) {
@@ -305,6 +501,84 @@ export class WaveformPlayer {
305
501
  }
306
502
  }
307
503
 
504
+ /**
505
+ * Load a new track
506
+ * @param {string} url - Audio URL
507
+ * @param {string} [title] - Track title
508
+ * @param {string} [subtitle] - Track subtitle
509
+ * @param {Object} [options] - Additional options
510
+ * @returns {Promise<void>}
511
+ */
512
+ async loadTrack(url, title = null, subtitle = null, options = {}) {
513
+ // Stop current playback and clear state
514
+ if (this.isPlaying) {
515
+ this.pause();
516
+ }
517
+
518
+ // Reset audio element completely
519
+ this.audio.src = '';
520
+ this.audio.load();
521
+
522
+ // Clear any errors
523
+ this.hasError = false;
524
+ if (this.errorEl) {
525
+ this.errorEl.style.display = 'none';
526
+ }
527
+ if (this.canvas) {
528
+ this.canvas.style.opacity = '1';
529
+ }
530
+ if (this.playBtn) {
531
+ this.playBtn.disabled = false;
532
+ }
533
+
534
+ // Reset state
535
+ this.progress = 0;
536
+ this.waveformData = [];
537
+
538
+ // Update options (including preload if specified)
539
+ this.options = mergeOptions(this.options, {
540
+ url,
541
+ title: title || this.options.title,
542
+ subtitle: subtitle || this.options.subtitle,
543
+ ...options
544
+ });
545
+
546
+ // Apply preload setting if it was changed
547
+ if (options.preload) {
548
+ this.audio.preload = options.preload;
549
+ }
550
+
551
+ // Update UI elements
552
+ if (this.subtitleEl) {
553
+ if (subtitle) {
554
+ this.subtitleEl.textContent = subtitle;
555
+ this.subtitleEl.style.display = '';
556
+ } else if (subtitle === '') {
557
+ this.subtitleEl.style.display = 'none';
558
+ }
559
+ }
560
+
561
+ // Update artwork if provided
562
+ if (options.artwork && this.artworkEl) {
563
+ this.artworkEl.src = options.artwork;
564
+ }
565
+
566
+ // Clear markers if new markers provided
567
+ if (options.markers) {
568
+ this.options.markers = options.markers;
569
+ }
570
+
571
+ // Load the new track
572
+ await this.load(url);
573
+
574
+ // Auto-play the new track
575
+ this.play();
576
+ }
577
+
578
+ // ============================================
579
+ // Visualization
580
+ // ============================================
581
+
308
582
  /**
309
583
  * Set waveform data
310
584
  * @private
@@ -354,6 +628,55 @@ export class WaveformPlayer {
354
628
  this.drawWaveform();
355
629
  }
356
630
 
631
+ /**
632
+ * Render markers on the waveform
633
+ * @private
634
+ */
635
+ renderMarkers() {
636
+ if (!this.options.showMarkers || !this.options.markers?.length || !this.markersContainer) return;
637
+
638
+ // Clear existing markers
639
+ this.markersContainer.innerHTML = '';
640
+
641
+ // Don't render if audio duration isn't available yet
642
+ if (!this.audio || !this.audio.duration || this.audio.duration === 0) {
643
+ return;
644
+ }
645
+
646
+ // Add each marker
647
+ this.options.markers.forEach((marker, index) => {
648
+ const position = (marker.time / this.audio.duration) * 100;
649
+
650
+ const markerEl = document.createElement('button');
651
+ markerEl.className = 'waveform-marker';
652
+ markerEl.style.left = `${position}%`;
653
+ markerEl.style.backgroundColor = marker.color || 'rgba(255, 255, 255, 0.5)';
654
+ markerEl.setAttribute('aria-label', marker.label);
655
+ markerEl.setAttribute('data-time', marker.time);
656
+
657
+ // Tooltip
658
+ const tooltip = document.createElement('span');
659
+ tooltip.className = 'waveform-marker-tooltip';
660
+ tooltip.textContent = marker.label;
661
+ markerEl.appendChild(tooltip);
662
+
663
+ // Click to seek
664
+ markerEl.addEventListener('click', (e) => {
665
+ e.stopPropagation();
666
+ this.seekTo(marker.time);
667
+ if (this.options.playOnSeek && !this.isPlaying) {
668
+ this.play();
669
+ }
670
+ });
671
+
672
+ this.markersContainer.appendChild(markerEl);
673
+ });
674
+ }
675
+
676
+ // ============================================
677
+ // Event Handlers
678
+ // ============================================
679
+
357
680
  /**
358
681
  * Handle canvas click
359
682
  * @private
@@ -387,6 +710,8 @@ export class WaveformPlayer {
387
710
  if (this.totalTimeEl) {
388
711
  this.totalTimeEl.textContent = formatTime(this.audio.duration);
389
712
  }
713
+ // Re-render markers when duration is known
714
+ this.renderMarkers();
390
715
  }
391
716
 
392
717
  /**
@@ -476,6 +801,10 @@ export class WaveformPlayer {
476
801
  }
477
802
  }
478
803
 
804
+ // ============================================
805
+ // Progress Updates
806
+ // ============================================
807
+
479
808
  /**
480
809
  * Start smooth update animation
481
810
  * @private
@@ -527,6 +856,10 @@ export class WaveformPlayer {
527
856
  }
528
857
  }
529
858
 
859
+ // ============================================
860
+ // UI Updates
861
+ // ============================================
862
+
530
863
  /**
531
864
  * Update BPM display
532
865
  * @private
@@ -538,6 +871,23 @@ export class WaveformPlayer {
538
871
  }
539
872
  }
540
873
 
874
+ /**
875
+ * Update speed UI to reflect current rate
876
+ * @private
877
+ */
878
+ updateSpeedUI() {
879
+ const speedValue = this.container.querySelector('.speed-value');
880
+ if (speedValue) {
881
+ const rate = this.audio.playbackRate;
882
+ speedValue.textContent = rate === 1 ? '1x' : `${rate}x`;
883
+ }
884
+
885
+ // Update active state in menu
886
+ this.container.querySelectorAll('.speed-option').forEach(btn => {
887
+ btn.classList.toggle('active', parseFloat(btn.dataset.rate) === this.audio.playbackRate);
888
+ });
889
+ }
890
+
541
891
  // ============================================
542
892
  // Public API
543
893
  // ============================================
@@ -576,6 +926,17 @@ export class WaveformPlayer {
576
926
  }
577
927
  }
578
928
 
929
+ /**
930
+ * Seek to time in seconds
931
+ * @param {number} seconds - Time in seconds
932
+ */
933
+ seekTo(seconds) {
934
+ if (this.audio && this.audio.duration) {
935
+ this.audio.currentTime = Math.max(0, Math.min(seconds, this.audio.duration));
936
+ this.updateProgress();
937
+ }
938
+ }
939
+
579
940
  /**
580
941
  * Seek to percentage
581
942
  * @param {number} percent - Percentage (0-1)
@@ -597,6 +958,20 @@ export class WaveformPlayer {
597
958
  }
598
959
  }
599
960
 
961
+ /**
962
+ * Set playback rate
963
+ * @param {number} rate - Playback rate (0.5 to 2)
964
+ */
965
+ setPlaybackRate(rate) {
966
+ if (!this.audio) return;
967
+
968
+ const clampedRate = Math.max(0.5, Math.min(2, rate));
969
+ this.audio.playbackRate = clampedRate;
970
+ this.options.playbackRate = clampedRate;
971
+
972
+ this.updateSpeedUI();
973
+ }
974
+
600
975
  /**
601
976
  * Destroy player instance
602
977
  */
@@ -618,7 +993,7 @@ export class WaveformPlayer {
618
993
  }
619
994
 
620
995
  // ============================================
621
- // Static methods
996
+ // Static Methods
622
997
  // ============================================
623
998
 
624
999
  /**
@@ -676,5 +1051,4 @@ export class WaveformPlayer {
676
1051
  throw error;
677
1052
  }
678
1053
  }
679
-
680
1054
  }
package/src/js/themes.js CHANGED
@@ -37,6 +37,15 @@ export const DEFAULT_OPTIONS = {
37
37
  url: '',
38
38
  height: 60,
39
39
  samples: 200,
40
+ preload: 'metadata',
41
+
42
+ // Playback
43
+ playbackRate: 1,
44
+ showPlaybackSpeed: false,
45
+ playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], // Available speeds
46
+
47
+ // Layout Options
48
+ buttonAlign: 'auto', // 'auto', 'top', 'center', 'bottom'
40
49
 
41
50
  // Default waveform style
42
51
  waveformStyle: 'mirror',
@@ -63,10 +72,17 @@ export const DEFAULT_OPTIONS = {
63
72
  showBPM: false,
64
73
  singlePlay: true,
65
74
  playOnSeek: true,
75
+ enableMediaSession: true,
76
+
77
+ // Markers
78
+ markers: [],
79
+ showMarkers: true,
66
80
 
67
81
  // Content
68
82
  title: null,
69
83
  subtitle: null,
84
+ artwork: null,
85
+ album: '',
70
86
 
71
87
  // Icons (SVG)
72
88
  playIcon: '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M8 5v14l11-7z"/></svg>',
package/src/js/utils.js CHANGED
@@ -15,11 +15,15 @@ export function parseDataAttributes(element) {
15
15
  if (element.dataset.url) options.url = element.dataset.url;
16
16
  if (element.dataset.height) options.height = parseInt(element.dataset.height);
17
17
  if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
18
+ if (element.dataset.preload) {
19
+ options.preload = element.dataset.preload;
20
+ }
18
21
 
19
22
  // Waveform style attributes
20
23
  if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
21
24
  if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
22
25
  if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
26
+ if (element.dataset.buttonAlign) options.buttonAlign = element.dataset.buttonAlign;
23
27
 
24
28
  // Color preset
25
29
  if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
@@ -46,13 +50,44 @@ export function parseDataAttributes(element) {
46
50
  if (element.dataset.singlePlay) options.singlePlay = element.dataset.singlePlay === 'true';
47
51
  if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === 'true';
48
52
 
49
- // Content
53
+ // Content and metadata
50
54
  if (element.dataset.title) options.title = element.dataset.title;
51
55
  if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
56
+ if (element.dataset.album) options.album = element.dataset.album;
57
+ if (element.dataset.artwork) options.artwork = element.dataset.artwork;
52
58
 
53
59
  // Waveform data
54
60
  if (element.dataset.waveform) options.waveform = element.dataset.waveform;
55
61
 
62
+ // Markers
63
+ if (element.dataset.markers) {
64
+ try {
65
+ options.markers = JSON.parse(element.dataset.markers);
66
+ } catch (e) {
67
+ console.warn('Invalid markers JSON:', e);
68
+ }
69
+ }
70
+
71
+ // Playback controls
72
+ if (element.dataset.playbackRate) {
73
+ options.playbackRate = parseFloat(element.dataset.playbackRate);
74
+ }
75
+ if (element.dataset.showPlaybackSpeed !== undefined) {
76
+ options.showPlaybackSpeed = element.dataset.showPlaybackSpeed === 'true';
77
+ }
78
+ if (element.dataset.playbackRates) {
79
+ try {
80
+ options.playbackRates = JSON.parse(element.dataset.playbackRates);
81
+ } catch (e) {
82
+ console.warn('Invalid playbackRates JSON:', e);
83
+ }
84
+ }
85
+
86
+ // Media Session API
87
+ if (element.dataset.enableMediaSession !== undefined) {
88
+ options.enableMediaSession = element.dataset.enableMediaSession === 'true';
89
+ }
90
+
56
91
  return options;
57
92
  }
58
93