vidply 1.0.26 → 1.0.27

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.
Files changed (45) hide show
  1. package/dist/dev/{vidply.HLSRenderer-X46P47LY.js → vidply.HLSRenderer-ENLZE4QS.js} +13 -8
  2. package/dist/dev/vidply.HLSRenderer-ENLZE4QS.js.map +7 -0
  3. package/dist/dev/{vidply.HTML5Renderer-LXQ3I45Q.js → vidply.HTML5Renderer-6SBDI6S2.js} +2 -2
  4. package/dist/dev/vidply.SoundCloudRenderer-CD7VJKNS.js +280 -0
  5. package/dist/dev/vidply.SoundCloudRenderer-CD7VJKNS.js.map +7 -0
  6. package/dist/dev/{vidply.VimeoRenderer-DCETT5IZ.js → vidply.VimeoRenderer-VPH4RNES.js} +17 -4
  7. package/dist/dev/vidply.VimeoRenderer-VPH4RNES.js.map +7 -0
  8. package/dist/dev/{vidply.YouTubeRenderer-QLMMD757.js → vidply.YouTubeRenderer-6MGKEFTZ.js} +14 -6
  9. package/dist/dev/vidply.YouTubeRenderer-6MGKEFTZ.js.map +7 -0
  10. package/dist/dev/{vidply.chunk-UEIJOJH6.js → vidply.chunk-BCOFCT6U.js} +4 -1
  11. package/dist/dev/vidply.chunk-BCOFCT6U.js.map +7 -0
  12. package/dist/dev/vidply.esm.js +288 -12
  13. package/dist/dev/vidply.esm.js.map +2 -2
  14. package/dist/legacy/vidply.js +609 -23
  15. package/dist/legacy/vidply.js.map +3 -3
  16. package/dist/legacy/vidply.min.js +1 -1
  17. package/dist/legacy/vidply.min.meta.json +26 -13
  18. package/dist/prod/vidply.HLSRenderer-CBXZ4RF2.min.js +6 -0
  19. package/dist/prod/{vidply.HTML5Renderer-XJCSUETP.min.js → vidply.HTML5Renderer-MY7XDV7R.min.js} +1 -1
  20. package/dist/prod/vidply.SoundCloudRenderer-MOR2CUFH.min.js +6 -0
  21. package/dist/prod/vidply.VimeoRenderer-3HBMM2WR.min.js +6 -0
  22. package/dist/prod/vidply.YouTubeRenderer-MFC2GMAC.min.js +6 -0
  23. package/dist/prod/vidply.chunk-OXXPY2XB.min.js +6 -0
  24. package/dist/prod/vidply.esm.min.js +6 -6
  25. package/dist/vidply.css +51 -0
  26. package/dist/vidply.esm.min.meta.json +56 -28
  27. package/dist/vidply.min.css +1 -1
  28. package/package.json +1 -1
  29. package/src/core/Player.js +117 -8
  30. package/src/features/PlaylistManager.js +312 -4
  31. package/src/renderers/HLSRenderer.js +17 -9
  32. package/src/renderers/HTML5Renderer.js +5 -0
  33. package/src/renderers/SoundCloudRenderer.js +355 -0
  34. package/src/renderers/VimeoRenderer.js +20 -4
  35. package/src/renderers/YouTubeRenderer.js +12 -6
  36. package/src/styles/vidply.css +51 -0
  37. package/dist/dev/vidply.HLSRenderer-X46P47LY.js.map +0 -7
  38. package/dist/dev/vidply.VimeoRenderer-DCETT5IZ.js.map +0 -7
  39. package/dist/dev/vidply.YouTubeRenderer-QLMMD757.js.map +0 -7
  40. package/dist/dev/vidply.chunk-UEIJOJH6.js.map +0 -7
  41. package/dist/prod/vidply.HLSRenderer-LDXSMWTI.min.js +0 -6
  42. package/dist/prod/vidply.VimeoRenderer-P3PU27S7.min.js +0 -6
  43. package/dist/prod/vidply.YouTubeRenderer-DGKKWB5M.min.js +0 -6
  44. package/dist/prod/vidply.chunk-BQBGEJF7.min.js +0 -6
  45. /package/dist/dev/{vidply.HTML5Renderer-LXQ3I45Q.js.map → vidply.HTML5Renderer-6SBDI6S2.js.map} +0 -0
@@ -28,6 +28,7 @@ export class PlaylistManager {
28
28
  autoPlayFirst: options.autoPlayFirst !== false, // Default true - auto-play first track on load
29
29
  loop: options.loop || false,
30
30
  showPanel: options.showPanel !== false, // Default true
31
+ recreatePlayers: options.recreatePlayers || false, // New: recreate player for each track type
31
32
  ...options
32
33
  };
33
34
 
@@ -41,6 +42,10 @@ export class PlaylistManager {
41
42
  // Track change guard to prevent cascade of next() calls
42
43
  this.isChangingTrack = false;
43
44
 
45
+ // Store the host element for player recreation
46
+ this.hostElement = options.hostElement || null;
47
+ this.PlayerClass = options.PlayerClass || null;
48
+
44
49
  // Bind methods
45
50
  this.handleTrackEnd = this.handleTrackEnd.bind(this);
46
51
  this.handleTrackError = this.handleTrackError.bind(this);
@@ -60,6 +65,221 @@ export class PlaylistManager {
60
65
  }
61
66
  }
62
67
 
68
+ /**
69
+ * Determine the media type for a track
70
+ * @param {Object} track - Track object
71
+ * @returns {string} - 'audio', 'video', 'youtube', 'vimeo', 'soundcloud', 'hls'
72
+ */
73
+ getTrackMediaType(track) {
74
+ const src = track.src || '';
75
+
76
+ if (src.includes('youtube.com') || src.includes('youtu.be')) {
77
+ return 'youtube';
78
+ }
79
+ if (src.includes('vimeo.com')) {
80
+ return 'vimeo';
81
+ }
82
+ if (src.includes('soundcloud.com') || src.includes('api.soundcloud.com')) {
83
+ return 'soundcloud';
84
+ }
85
+ if (src.includes('.m3u8')) {
86
+ return 'hls';
87
+ }
88
+ if (track.type && track.type.startsWith('audio/')) {
89
+ return 'audio';
90
+ }
91
+ // Default to video for video types or unknown
92
+ return 'video';
93
+ }
94
+
95
+ /**
96
+ * Recreate the player with the appropriate element type for the track
97
+ * @param {Object} track - Track to load
98
+ * @param {boolean} autoPlay - Whether to auto-play after creation
99
+ */
100
+ async recreatePlayerForTrack(track, autoPlay = false) {
101
+ if (!this.hostElement || !this.PlayerClass) {
102
+ console.warn('VidPly Playlist: Cannot recreate player - missing hostElement or PlayerClass');
103
+ return false;
104
+ }
105
+
106
+ const mediaType = this.getTrackMediaType(track);
107
+ // SoundCloud uses an iframe widget, so it doesn't need an audio element
108
+ // Only local audio files need an actual <audio> element
109
+ const elementType = (mediaType === 'audio') ? 'audio' : 'video';
110
+
111
+ // Store playlist panel state
112
+ const wasVisible = this.isPanelVisible;
113
+ const savedTracks = [...this.tracks]; // Keep track data
114
+ const savedIndex = this.currentIndex;
115
+
116
+ // Detach all playlist UI elements from DOM (keep references)
117
+ // These will be reattached to the new player container
118
+ if (this.trackArtworkElement && this.trackArtworkElement.parentNode) {
119
+ this.trackArtworkElement.parentNode.removeChild(this.trackArtworkElement);
120
+ }
121
+ if (this.trackInfoElement && this.trackInfoElement.parentNode) {
122
+ this.trackInfoElement.parentNode.removeChild(this.trackInfoElement);
123
+ }
124
+ if (this.navigationFeedback && this.navigationFeedback.parentNode) {
125
+ this.navigationFeedback.parentNode.removeChild(this.navigationFeedback);
126
+ }
127
+ if (this.playlistPanel && this.playlistPanel.parentNode) {
128
+ this.playlistPanel.parentNode.removeChild(this.playlistPanel);
129
+ }
130
+
131
+ // Remove event listeners before destroying
132
+ if (this.player) {
133
+ this.player.off('ended', this.handleTrackEnd);
134
+ this.player.off('error', this.handleTrackError);
135
+ this.player.destroy();
136
+ }
137
+
138
+ // Clear the host element
139
+ this.hostElement.innerHTML = '';
140
+
141
+ // Create new media element with appropriate type
142
+ const mediaElement = document.createElement(elementType);
143
+ mediaElement.setAttribute('preload', 'metadata');
144
+
145
+ // For video elements with local media, set poster
146
+ if (elementType === 'video' && track.poster &&
147
+ (mediaType === 'video' || mediaType === 'hls')) {
148
+ mediaElement.setAttribute('poster', track.poster);
149
+ }
150
+
151
+ // For external renderers (YouTube, Vimeo, SoundCloud, HLS), don't add source
152
+ // The renderer will handle the source directly
153
+ const isExternalRenderer = ['youtube', 'vimeo', 'soundcloud', 'hls'].includes(mediaType);
154
+
155
+ if (!isExternalRenderer) {
156
+ // Add source for HTML5 media
157
+ const source = document.createElement('source');
158
+ source.src = track.src;
159
+ if (track.type) {
160
+ source.type = track.type;
161
+ }
162
+ mediaElement.appendChild(source);
163
+
164
+ // Add tracks (captions, chapters, etc.)
165
+ if (track.tracks && track.tracks.length > 0) {
166
+ track.tracks.forEach(trackConfig => {
167
+ const trackEl = document.createElement('track');
168
+ trackEl.src = trackConfig.src;
169
+ trackEl.kind = trackConfig.kind || 'captions';
170
+ trackEl.srclang = trackConfig.srclang || 'en';
171
+ trackEl.label = trackConfig.label || trackConfig.srclang;
172
+ if (trackConfig.default) {
173
+ trackEl.default = true;
174
+ }
175
+ mediaElement.appendChild(trackEl);
176
+ });
177
+ }
178
+ }
179
+
180
+ this.hostElement.appendChild(mediaElement);
181
+
182
+ // Create new player with the media element
183
+ // Pass the source for external renderers via options
184
+ const playerOptions = {
185
+ mediaType: elementType,
186
+ poster: track.poster,
187
+ audioDescriptionSrc: track.audioDescriptionSrc || null,
188
+ audioDescriptionDuration: track.audioDescriptionDuration || null,
189
+ signLanguageSrc: track.signLanguageSrc || null
190
+ };
191
+
192
+ this.player = new this.PlayerClass(mediaElement, playerOptions);
193
+
194
+ // Re-register playlist manager
195
+ this.player.playlistManager = this;
196
+
197
+ // Wait for player to be ready
198
+ await new Promise(resolve => {
199
+ this.player.on('ready', resolve);
200
+ });
201
+
202
+ // Re-attach event listeners
203
+ this.player.on('ended', this.handleTrackEnd);
204
+ this.player.on('error', this.handleTrackError);
205
+
206
+ // Re-attach all playlist UI elements to the new player's container
207
+ if (this.player.container) {
208
+ // Track artwork goes before video wrapper
209
+ if (this.trackArtworkElement) {
210
+ const videoWrapper = this.player.container.querySelector('.vidply-video-wrapper');
211
+ if (videoWrapper) {
212
+ this.player.container.insertBefore(this.trackArtworkElement, videoWrapper);
213
+ } else {
214
+ this.player.container.appendChild(this.trackArtworkElement);
215
+ }
216
+ }
217
+ // Track info
218
+ if (this.trackInfoElement) {
219
+ this.player.container.appendChild(this.trackInfoElement);
220
+ }
221
+ // Navigation feedback (screen reader only)
222
+ if (this.navigationFeedback) {
223
+ this.player.container.appendChild(this.navigationFeedback);
224
+ }
225
+ // Playlist panel
226
+ if (this.playlistPanel) {
227
+ this.player.container.appendChild(this.playlistPanel);
228
+ }
229
+ }
230
+
231
+ // Update container reference
232
+ this.container = this.player.container;
233
+
234
+ // Update controls (adds playlist prev/next buttons)
235
+ this.updatePlayerControls();
236
+
237
+ // Restore tracks data (we kept it during recreation)
238
+ this.tracks = savedTracks;
239
+ this.currentIndex = savedIndex;
240
+
241
+ // Update playlist UI to reflect current state
242
+ this.updatePlaylistUI();
243
+
244
+ // Restore playlist panel visibility
245
+ this.isPanelVisible = wasVisible;
246
+ if (this.playlistPanel) {
247
+ this.playlistPanel.style.display = wasVisible ? '' : 'none';
248
+ }
249
+
250
+ // For external renderers, load the track via player.load()
251
+ // For HTML5, the source is already set on the element
252
+ if (isExternalRenderer) {
253
+ this.player.load({
254
+ src: track.src,
255
+ type: track.type,
256
+ poster: track.poster,
257
+ tracks: track.tracks || [],
258
+ audioDescriptionSrc: track.audioDescriptionSrc || null,
259
+ signLanguageSrc: track.signLanguageSrc || null
260
+ });
261
+ } else {
262
+ // For HTML5 media, also load to set up accessibility features
263
+ this.player.load({
264
+ src: track.src,
265
+ type: track.type,
266
+ poster: track.poster,
267
+ tracks: track.tracks || [],
268
+ audioDescriptionSrc: track.audioDescriptionSrc || null,
269
+ signLanguageSrc: track.signLanguageSrc || null
270
+ });
271
+ }
272
+
273
+ // Auto-play if requested
274
+ if (autoPlay) {
275
+ setTimeout(() => {
276
+ this.player.play();
277
+ }, 100);
278
+ }
279
+
280
+ return true;
281
+ }
282
+
63
283
  init() {
64
284
  // Listen for track end
65
285
  this.player.on('ended', this.handleTrackEnd);
@@ -218,7 +438,7 @@ export class PlaylistManager {
218
438
  * Load a track without playing
219
439
  * @param {number} index - Track index
220
440
  */
221
- loadTrack(index) {
441
+ async loadTrack(index) {
222
442
  if (index < 0 || index >= this.tracks.length) {
223
443
  console.warn('VidPly Playlist: Invalid track index', index);
224
444
  return;
@@ -232,7 +452,36 @@ export class PlaylistManager {
232
452
  // Update current index
233
453
  this.currentIndex = index;
234
454
 
235
- // Load track into player
455
+ // Check if we should recreate the player for this track type
456
+ if (this.options.recreatePlayers && this.hostElement && this.PlayerClass) {
457
+ const currentMediaType = this.player ?
458
+ (this.player.element.tagName === 'AUDIO' ? 'audio' : 'video') : null;
459
+ const newMediaType = this.getTrackMediaType(track);
460
+ const newElementType = (newMediaType === 'audio' || newMediaType === 'soundcloud') ? 'audio' : 'video';
461
+
462
+ // Recreate if element type is different
463
+ if (currentMediaType !== newElementType) {
464
+ await this.recreatePlayerForTrack(track, false);
465
+ // Update UI after recreation
466
+ this.updateTrackInfo(track);
467
+ this.updatePlaylistUI();
468
+
469
+ // Emit event
470
+ this.player.emit('playlisttrackchange', {
471
+ index: index,
472
+ item: track,
473
+ total: this.tracks.length
474
+ });
475
+
476
+ // Clear guard flag
477
+ setTimeout(() => {
478
+ this.isChangingTrack = false;
479
+ }, 150);
480
+ return;
481
+ }
482
+ }
483
+
484
+ // Load track into player (normal path)
236
485
  this.player.load({
237
486
  src: track.src,
238
487
  type: track.type,
@@ -264,7 +513,7 @@ export class PlaylistManager {
264
513
  * @param {number} index - Track index
265
514
  * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
266
515
  */
267
- play(index, userInitiated = false) {
516
+ async play(index, userInitiated = false) {
268
517
  if (index < 0 || index >= this.tracks.length) {
269
518
  console.warn('VidPly Playlist: Invalid track index', index);
270
519
  return;
@@ -278,7 +527,36 @@ export class PlaylistManager {
278
527
  // Update current index
279
528
  this.currentIndex = index;
280
529
 
281
- // Load track into player
530
+ // Check if we should recreate the player for this track type
531
+ if (this.options.recreatePlayers && this.hostElement && this.PlayerClass) {
532
+ const currentMediaType = this.player ?
533
+ (this.player.element.tagName === 'AUDIO' ? 'audio' : 'video') : null;
534
+ const newMediaType = this.getTrackMediaType(track);
535
+ const newElementType = (newMediaType === 'audio' || newMediaType === 'soundcloud') ? 'audio' : 'video';
536
+
537
+ // Recreate if element type is different
538
+ if (currentMediaType !== newElementType) {
539
+ await this.recreatePlayerForTrack(track, true); // true = autoPlay
540
+ // Update UI after recreation
541
+ this.updateTrackInfo(track);
542
+ this.updatePlaylistUI();
543
+
544
+ // Emit event
545
+ this.player.emit('playlisttrackchange', {
546
+ index: index,
547
+ item: track,
548
+ total: this.tracks.length
549
+ });
550
+
551
+ // Clear guard flag
552
+ setTimeout(() => {
553
+ this.isChangingTrack = false;
554
+ }, 150);
555
+ return;
556
+ }
557
+ }
558
+
559
+ // Load track into player (normal path)
282
560
  this.player.load({
283
561
  src: track.src,
284
562
  type: track.type,
@@ -358,10 +636,40 @@ export class PlaylistManager {
358
636
  }
359
637
  }
360
638
 
639
+ /**
640
+ * Check if a source URL requires an external renderer
641
+ * @param {string} src - Source URL
642
+ * @returns {boolean}
643
+ */
644
+ isExternalRendererUrl(src) {
645
+ if (!src) return false;
646
+ return src.includes('youtube.com') ||
647
+ src.includes('youtu.be') ||
648
+ src.includes('vimeo.com') ||
649
+ src.includes('soundcloud.com') ||
650
+ src.includes('api.soundcloud.com') ||
651
+ src.includes('.m3u8');
652
+ }
653
+
361
654
  /**
362
655
  * Handle track error
363
656
  */
364
657
  handleTrackError(e) {
658
+ // Don't auto-advance for external renderer tracks
659
+ // External renderers (YouTube, Vimeo, SoundCloud, HLS) may trigger HTML5 errors
660
+ // that should be ignored since the external renderer handles playback
661
+ const currentTrack = this.getCurrentTrack();
662
+ if (currentTrack && currentTrack.src && this.isExternalRendererUrl(currentTrack.src)) {
663
+ // Silently ignore errors for external renderer tracks
664
+ return;
665
+ }
666
+
667
+ // Don't auto-advance if we're in the process of changing tracks
668
+ // This prevents a cascade of next() calls when switching between renderer types
669
+ if (this.isChangingTrack) {
670
+ return;
671
+ }
672
+
365
673
  console.error('VidPly Playlist: Track error', e);
366
674
 
367
675
  // Try next track
@@ -92,15 +92,18 @@ export class HLSRenderer {
92
92
  // Attach media element
93
93
  this.hls.attachMedia(this.media);
94
94
 
95
- // Load source - Get from attribute to avoid blob URL conversion
96
- let src;
97
- const sourceElement = this.player.element.querySelector('source');
98
- if (sourceElement) {
99
- // Use getAttribute to get the original URL, not the blob-converted one
100
- src = sourceElement.getAttribute('src');
101
- } else {
102
- // Fallback to element's src attribute
103
- src = this.player.element.getAttribute('src') || this.player.element.src;
95
+ // Load source - use currentSource for external renderers, or get from attribute
96
+ let src = this.player.currentSource;
97
+
98
+ if (!src) {
99
+ const sourceElement = this.player.element.querySelector('source');
100
+ if (sourceElement) {
101
+ // Use getAttribute to get the original URL, not the blob-converted one
102
+ src = sourceElement.getAttribute('src');
103
+ } else {
104
+ // Fallback to element's src attribute
105
+ src = this.player.element.getAttribute('src') || this.player.element.src;
106
+ }
104
107
  }
105
108
 
106
109
  this.player.log(`Loading HLS source: ${src}`, 'log');
@@ -130,6 +133,11 @@ export class HLSRenderer {
130
133
  this.hls.on(window.Hls.Events.MANIFEST_PARSED, (event, data) => {
131
134
  this.player.log('HLS manifest loaded, found ' + data.levels.length + ' quality levels');
132
135
  this.player.emit('hlsmanifestparsed', data);
136
+
137
+ // Show VidPly controls (remove external controls class if present)
138
+ if (this.player.container) {
139
+ this.player.container.classList.remove('vidply-external-controls');
140
+ }
133
141
  });
134
142
 
135
143
  this.hls.on(window.Hls.Events.LEVEL_SWITCHED, (event, data) => {
@@ -20,6 +20,11 @@ export class HTML5Renderer {
20
20
 
21
21
  // Load media
22
22
  this.media.load();
23
+
24
+ // Show VidPly controls (remove external controls class if present)
25
+ if (this.player.container) {
26
+ this.player.container.classList.remove('vidply-external-controls');
27
+ }
23
28
  }
24
29
 
25
30
  attachEvents() {