vidply 1.0.32 → 1.0.33
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 +7 -7
- package/dist/dev/{vidply.HLSRenderer-5MJZR4D2.js → vidply.HLSRenderer-LIFBU6UD.js} +48 -3
- package/dist/dev/vidply.HLSRenderer-LIFBU6UD.js.map +7 -0
- package/dist/dev/{vidply.HTML5Renderer-FXBZQL6Y.js → vidply.HTML5Renderer-YWMVYWFS.js} +2 -2
- package/dist/dev/{vidply.TranscriptManager-T677KF4N.js → vidply.TranscriptManager-R7NJRU7E.js} +2 -2
- package/dist/dev/{vidply.chunk-GS2JX5RQ.js → vidply.chunk-PMRKJBGH.js} +5 -2
- package/dist/dev/vidply.chunk-PMRKJBGH.js.map +7 -0
- package/dist/dev/{vidply.chunk-W2LSBD6Y.js → vidply.chunk-UVO24MXU.js} +33 -3
- package/dist/dev/vidply.chunk-UVO24MXU.js.map +7 -0
- package/dist/dev/{vidply.de-SNL6AJ4D.js → vidply.de-CEGBLV67.js} +4 -1
- package/dist/dev/vidply.de-CEGBLV67.js.map +7 -0
- package/dist/dev/vidply.esm.js +364 -63
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/legacy/vidply.js +459 -63
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +1 -1
- package/dist/legacy/vidply.min.meta.json +15 -15
- package/dist/prod/vidply.HLSRenderer-ESR6NAMI.min.js +6 -0
- package/dist/prod/{vidply.HTML5Renderer-KKW3OLHM.min.js → vidply.HTML5Renderer-6ROXQSQY.min.js} +1 -1
- package/dist/prod/{vidply.TranscriptManager-WFZSW6NR.min.js → vidply.TranscriptManager-B65LKXGG.min.js} +1 -1
- package/dist/prod/vidply.chunk-7HVHEUHH.min.js +6 -0
- package/dist/prod/vidply.chunk-IQKD4GUB.min.js +6 -0
- package/dist/prod/vidply.de-IHKC573T.min.js +6 -0
- package/dist/prod/vidply.esm.min.js +4 -4
- package/dist/vidply.esm.min.meta.json +33 -33
- package/package.json +1 -1
- package/src/controls/ControlBar.js +104 -70
- package/src/core/Player.js +217 -3
- package/src/features/PlaylistManager.js +175 -17
- package/src/i18n/languages/de.js +3 -0
- package/src/i18n/languages/en.js +3 -0
- package/src/renderers/HLSRenderer.js +60 -1
- package/src/renderers/HTML5Renderer.js +43 -5
- package/dist/dev/vidply.HLSRenderer-5MJZR4D2.js.map +0 -7
- package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +0 -7
- package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +0 -7
- package/dist/dev/vidply.de-SNL6AJ4D.js.map +0 -7
- package/dist/prod/vidply.HLSRenderer-VWNJD2CB.min.js +0 -6
- package/dist/prod/vidply.chunk-34RH2THY.min.js +0 -6
- package/dist/prod/vidply.chunk-LGTJRPUL.min.js +0 -6
- package/dist/prod/vidply.de-FR3XX54P.min.js +0 -6
- /package/dist/dev/{vidply.HTML5Renderer-FXBZQL6Y.js.map → vidply.HTML5Renderer-YWMVYWFS.js.map} +0 -0
- /package/dist/dev/{vidply.TranscriptManager-T677KF4N.js.map → vidply.TranscriptManager-R7NJRU7E.js.map} +0 -0
package/src/core/Player.js
CHANGED
|
@@ -82,6 +82,17 @@ export class Player extends EventEmitter {
|
|
|
82
82
|
volume: 0.8,
|
|
83
83
|
playbackSpeed: 1.0,
|
|
84
84
|
preload: 'metadata',
|
|
85
|
+
// Optional initial duration (seconds) so UI can show duration
|
|
86
|
+
// before media metadata is loaded (useful with deferLoad/preload=none).
|
|
87
|
+
initialDuration: 0,
|
|
88
|
+
// When enabled, VidPly will not start network loading during init().
|
|
89
|
+
// - HTML5: does not call element.load() until the first user-initiated play()
|
|
90
|
+
// - HLS (hls.js): does not load manifest/segments until the first play()
|
|
91
|
+
// This is useful for pages with many players to avoid high initial bandwidth.
|
|
92
|
+
deferLoad: false,
|
|
93
|
+
// When enabled, clicking Audio Description / Sign Language before playback will show
|
|
94
|
+
// a notice instead of implicitly starting playback/loading.
|
|
95
|
+
requirePlaybackForAccessibilityToggles: false,
|
|
85
96
|
startTime: 0,
|
|
86
97
|
playsInline: true, // Enable inline playback on iOS (prevents native fullscreen)
|
|
87
98
|
|
|
@@ -190,6 +201,10 @@ export class Player extends EventEmitter {
|
|
|
190
201
|
this.options.metadataAlerts = this.options.metadataAlerts || {};
|
|
191
202
|
this.options.metadataHashtags = this.options.metadataHashtags || {};
|
|
192
203
|
|
|
204
|
+
// Notice UI
|
|
205
|
+
this.noticeElement = null;
|
|
206
|
+
this.noticeTimeout = null;
|
|
207
|
+
|
|
193
208
|
// Storage manager
|
|
194
209
|
this.storage = new StorageManager('vidply');
|
|
195
210
|
|
|
@@ -209,10 +224,11 @@ export class Player extends EventEmitter {
|
|
|
209
224
|
ended: false,
|
|
210
225
|
buffering: false,
|
|
211
226
|
seeking: false,
|
|
227
|
+
hasStartedPlayback: false,
|
|
212
228
|
muted: this.options.muted,
|
|
213
229
|
volume: this.options.volume,
|
|
214
230
|
currentTime: 0,
|
|
215
|
-
duration: 0,
|
|
231
|
+
duration: Number(this.options.initialDuration) > 0 ? Number(this.options.initialDuration) : 0,
|
|
216
232
|
playbackSpeed: this.options.playbackSpeed,
|
|
217
233
|
fullscreen: false,
|
|
218
234
|
pip: false,
|
|
@@ -305,6 +321,59 @@ export class Player extends EventEmitter {
|
|
|
305
321
|
this.init();
|
|
306
322
|
}
|
|
307
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Show a small in-player notice (non-blocking), also announced to screen readers.
|
|
326
|
+
*/
|
|
327
|
+
showNotice(message, { timeout = 2500, priority = 'polite' } = {}) {
|
|
328
|
+
try {
|
|
329
|
+
if (!message) return;
|
|
330
|
+
if (!this.container) return;
|
|
331
|
+
|
|
332
|
+
// Screen reader announcement (reuse KeyboardManager announcer if available)
|
|
333
|
+
if (this.keyboardManager?.announce) {
|
|
334
|
+
this.keyboardManager.announce(message, priority);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!this.noticeElement) {
|
|
338
|
+
const el = document.createElement('div');
|
|
339
|
+
el.className = `${this.options.classPrefix}-notice`;
|
|
340
|
+
el.setAttribute('role', 'status');
|
|
341
|
+
el.setAttribute('aria-live', priority);
|
|
342
|
+
el.setAttribute('aria-atomic', 'true');
|
|
343
|
+
// Inline styling to avoid requiring CSS rebuilds
|
|
344
|
+
el.style.position = 'absolute';
|
|
345
|
+
el.style.left = '0.75rem';
|
|
346
|
+
el.style.right = '0.75rem';
|
|
347
|
+
el.style.top = '0.75rem';
|
|
348
|
+
el.style.zIndex = '9999';
|
|
349
|
+
el.style.padding = '0.5rem 0.75rem';
|
|
350
|
+
el.style.borderRadius = '0.5rem';
|
|
351
|
+
el.style.background = 'rgba(0, 0, 0, 0.75)';
|
|
352
|
+
el.style.color = '#fff';
|
|
353
|
+
el.style.fontSize = '0.875rem';
|
|
354
|
+
el.style.lineHeight = '1.3';
|
|
355
|
+
el.style.pointerEvents = 'none';
|
|
356
|
+
this.noticeElement = el;
|
|
357
|
+
this.container.appendChild(el);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.noticeElement.textContent = message;
|
|
361
|
+
this.noticeElement.style.display = 'block';
|
|
362
|
+
|
|
363
|
+
if (this.noticeTimeout) {
|
|
364
|
+
clearTimeout(this.noticeTimeout);
|
|
365
|
+
this.noticeTimeout = null;
|
|
366
|
+
}
|
|
367
|
+
this.noticeTimeout = setTimeout(() => {
|
|
368
|
+
if (this.noticeElement) {
|
|
369
|
+
this.noticeElement.style.display = 'none';
|
|
370
|
+
}
|
|
371
|
+
}, timeout);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// ignore
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
308
377
|
async init() {
|
|
309
378
|
try {
|
|
310
379
|
this.log('Initializing VidPly player');
|
|
@@ -570,9 +639,35 @@ export class Player extends EventEmitter {
|
|
|
570
639
|
: this.options.height;
|
|
571
640
|
}
|
|
572
641
|
|
|
642
|
+
// If no explicit height is set, ensure video players still have a stable layout box
|
|
643
|
+
// even before any media is loaded (important for deferLoad + playlists).
|
|
644
|
+
// We use the element's width/height attributes (e.g. from TYPO3) as aspect ratio.
|
|
645
|
+
if (this.element.tagName === 'VIDEO' && !this.options.height) {
|
|
646
|
+
const wAttr = parseInt(this.element.getAttribute('width') || '', 10);
|
|
647
|
+
const hAttr = parseInt(this.element.getAttribute('height') || '', 10);
|
|
648
|
+
if (Number.isFinite(wAttr) && Number.isFinite(hAttr) && wAttr > 0 && hAttr > 0) {
|
|
649
|
+
// Only set if not already defined by CSS/inline style
|
|
650
|
+
if (!this.container.style.aspectRatio) {
|
|
651
|
+
this.container.style.aspectRatio = `${wAttr} / ${hAttr}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// The actual visual box is the videoWrapper (the video element is 100% height).
|
|
655
|
+
// Give the wrapper the same aspect ratio so posters render correctly before metadata is loaded.
|
|
656
|
+
if (this.videoWrapper && !this.videoWrapper.style.aspectRatio) {
|
|
657
|
+
this.videoWrapper.style.aspectRatio = `${wAttr} / ${hAttr}`;
|
|
658
|
+
// Override default CSS height:100% (which depends on parent having a height)
|
|
659
|
+
this.videoWrapper.style.height = 'auto';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
573
664
|
// Set poster (convert relative paths to absolute URLs)
|
|
574
665
|
if (this.options.poster && this.element.tagName === 'VIDEO') {
|
|
575
|
-
|
|
666
|
+
const resolvedPoster = this.resolvePosterPath(this.options.poster);
|
|
667
|
+
this.element.poster = resolvedPoster;
|
|
668
|
+
// If we intentionally have no media loaded yet (e.g. deferLoad/playlist),
|
|
669
|
+
// use poster aspect ratio to size the wrapper so the poster isn't stretched.
|
|
670
|
+
this.applyPosterAspectRatio(resolvedPoster);
|
|
576
671
|
}
|
|
577
672
|
|
|
578
673
|
// Create centered play button overlay (only for video)
|
|
@@ -596,6 +691,7 @@ export class Player extends EventEmitter {
|
|
|
596
691
|
});
|
|
597
692
|
|
|
598
693
|
this.on('play', () => {
|
|
694
|
+
this.state.hasStartedPlayback = true;
|
|
599
695
|
// Hide poster immediately when playing
|
|
600
696
|
this.hidePosterOverlay();
|
|
601
697
|
});
|
|
@@ -615,6 +711,45 @@ export class Player extends EventEmitter {
|
|
|
615
711
|
}, { once: true });
|
|
616
712
|
}
|
|
617
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Apply aspect ratio to the video wrapper based on the poster's intrinsic size.
|
|
716
|
+
* This helps render correct poster sizing before media metadata is available.
|
|
717
|
+
*/
|
|
718
|
+
applyPosterAspectRatio(posterUrl) {
|
|
719
|
+
try {
|
|
720
|
+
if (!posterUrl) return;
|
|
721
|
+
if (this.element.tagName !== 'VIDEO') return;
|
|
722
|
+
if (!this.videoWrapper) return;
|
|
723
|
+
|
|
724
|
+
// If user explicitly configured dimensions, don't override.
|
|
725
|
+
if (this.options.width || this.options.height) return;
|
|
726
|
+
|
|
727
|
+
// Avoid repeated work
|
|
728
|
+
if (this._posterAspectAppliedFor === posterUrl) return;
|
|
729
|
+
this._posterAspectAppliedFor = posterUrl;
|
|
730
|
+
|
|
731
|
+
const img = new Image();
|
|
732
|
+
img.decoding = 'async';
|
|
733
|
+
img.onload = () => {
|
|
734
|
+
const w = img.naturalWidth;
|
|
735
|
+
const h = img.naturalHeight;
|
|
736
|
+
if (!w || !h) return;
|
|
737
|
+
|
|
738
|
+
// Apply to wrapper (the actual layout box)
|
|
739
|
+
this.videoWrapper.style.aspectRatio = `${w} / ${h}`;
|
|
740
|
+
this.videoWrapper.style.height = 'auto';
|
|
741
|
+
|
|
742
|
+
// Also apply to container if not explicitly set
|
|
743
|
+
if (this.container && !this.container.style.aspectRatio) {
|
|
744
|
+
this.container.style.aspectRatio = `${w} / ${h}`;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
img.src = posterUrl;
|
|
748
|
+
} catch (e) {
|
|
749
|
+
// ignore
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
618
753
|
createPlayButtonOverlay() {
|
|
619
754
|
// Create complete SVG play button from Icons.js
|
|
620
755
|
this.playButtonOverlay = createPlayOverlay();
|
|
@@ -1174,7 +1309,30 @@ export class Player extends EventEmitter {
|
|
|
1174
1309
|
} else {
|
|
1175
1310
|
// Just reload the current renderer with the updated element
|
|
1176
1311
|
this.renderer.media = this.element; // Update media reference
|
|
1177
|
-
this.
|
|
1312
|
+
if (this.options.deferLoad) {
|
|
1313
|
+
try {
|
|
1314
|
+
// Keep configured preload behavior; actual network load is controlled
|
|
1315
|
+
// by ensureLoaded()/play() when deferLoad is enabled.
|
|
1316
|
+
this.element.preload = this.options.preload || 'metadata';
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
// ignore
|
|
1319
|
+
}
|
|
1320
|
+
// Reset renderer-level deferred flags if present (HTML5/HLS renderers)
|
|
1321
|
+
if (this.renderer) {
|
|
1322
|
+
if (typeof this.renderer._didDeferredLoad === 'boolean') {
|
|
1323
|
+
this.renderer._didDeferredLoad = false;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof this.renderer._hlsSourceLoaded === 'boolean') {
|
|
1326
|
+
this.renderer._hlsSourceLoaded = false;
|
|
1327
|
+
}
|
|
1328
|
+
if ('_pendingSrc' in this.renderer) {
|
|
1329
|
+
// For HLS, store pending src for the first play() call
|
|
1330
|
+
this.renderer._pendingSrc = this._pendingSource || this.currentSource || null;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
this.element.load();
|
|
1335
|
+
}
|
|
1178
1336
|
}
|
|
1179
1337
|
|
|
1180
1338
|
// Clear the renderer switching flag after a delay to catch async errors
|
|
@@ -1245,6 +1403,22 @@ export class Player extends EventEmitter {
|
|
|
1245
1403
|
}
|
|
1246
1404
|
}
|
|
1247
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* Ensure the current renderer has started its initial load (metadata/manifest)
|
|
1408
|
+
* without starting playback. This is useful for playlists to behave like
|
|
1409
|
+
* single videos on selection, while still keeping autoplay off.
|
|
1410
|
+
*/
|
|
1411
|
+
ensureLoaded() {
|
|
1412
|
+
try {
|
|
1413
|
+
if (!this.renderer) return;
|
|
1414
|
+
if (typeof this.renderer.ensureLoaded === 'function') {
|
|
1415
|
+
this.renderer.ensureLoaded();
|
|
1416
|
+
}
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
// ignore
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1248
1422
|
/**
|
|
1249
1423
|
* Check if we need to change renderer type
|
|
1250
1424
|
* @param {string} src - New source URL
|
|
@@ -1293,6 +1467,14 @@ export class Player extends EventEmitter {
|
|
|
1293
1467
|
play() {
|
|
1294
1468
|
if (this.renderer) {
|
|
1295
1469
|
this.renderer.play();
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Playlist support: if no renderer exists yet (no initial src),
|
|
1474
|
+
// start playback via playlist selection.
|
|
1475
|
+
if (this.playlistManager && Array.isArray(this.playlistManager.tracks) && this.playlistManager.tracks.length > 0) {
|
|
1476
|
+
const index = this.playlistManager.currentIndex >= 0 ? this.playlistManager.currentIndex : 0;
|
|
1477
|
+
this.playlistManager.play(index, true);
|
|
1296
1478
|
}
|
|
1297
1479
|
}
|
|
1298
1480
|
|
|
@@ -3139,6 +3321,22 @@ export class Player extends EventEmitter {
|
|
|
3139
3321
|
}
|
|
3140
3322
|
|
|
3141
3323
|
async toggleAudioDescription() {
|
|
3324
|
+
if (this.options.requirePlaybackForAccessibilityToggles && !this.renderer && this.playlistManager?.tracks?.length) {
|
|
3325
|
+
this.showNotice(i18n.t('player.startPlaybackForAudioDescription'));
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// If user toggles audio description before the first track has been loaded,
|
|
3330
|
+
// remember desired state and start playback so the described source is loaded.
|
|
3331
|
+
if (!this.renderer && this.playlistManager && this.playlistManager.tracks?.length) {
|
|
3332
|
+
this.audioDescriptionManager.desiredState = !this.audioDescriptionManager.desiredState;
|
|
3333
|
+
this.state.audioDescriptionEnabled = this.audioDescriptionManager.desiredState;
|
|
3334
|
+
this.emit(this.audioDescriptionManager.desiredState ? 'audiodescriptionenabled' : 'audiodescriptiondisabled');
|
|
3335
|
+
// Start playback (PlaylistManager.play() will honor desiredState and load described src)
|
|
3336
|
+
this.play();
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3142
3340
|
return this.audioDescriptionManager.toggle();
|
|
3143
3341
|
}
|
|
3144
3342
|
|
|
@@ -3488,6 +3686,22 @@ export class Player extends EventEmitter {
|
|
|
3488
3686
|
}
|
|
3489
3687
|
|
|
3490
3688
|
toggleSignLanguage() {
|
|
3689
|
+
if (this.options.requirePlaybackForAccessibilityToggles && !this.renderer && this.playlistManager?.tracks?.length) {
|
|
3690
|
+
this.showNotice(i18n.t('player.startPlaybackForSignLanguage'));
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// If user toggles sign language before the first track has been loaded,
|
|
3695
|
+
// enable the overlay and start playback so sign language video sync begins.
|
|
3696
|
+
if (!this.renderer && this.playlistManager && this.playlistManager.tracks?.length) {
|
|
3697
|
+
const wasEnabled = this.signLanguageManager.enabled;
|
|
3698
|
+
const result = this.signLanguageManager.toggle();
|
|
3699
|
+
if (!wasEnabled && this.signLanguageManager.enabled) {
|
|
3700
|
+
this.play();
|
|
3701
|
+
}
|
|
3702
|
+
return result;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3491
3705
|
return this.signLanguageManager.toggle();
|
|
3492
3706
|
}
|
|
3493
3707
|
|
|
@@ -128,6 +128,9 @@ export class PlaylistManager {
|
|
|
128
128
|
this.playlistPanel.parentNode.removeChild(this.playlistPanel);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Preserve existing player options so recreated players behave consistently
|
|
132
|
+
const preservedPlayerOptions = this.player?.options ? { ...this.player.options } : {};
|
|
133
|
+
|
|
131
134
|
// Remove event listeners before destroying
|
|
132
135
|
if (this.player) {
|
|
133
136
|
this.player.off('ended', this.handleTrackEnd);
|
|
@@ -140,7 +143,9 @@ export class PlaylistManager {
|
|
|
140
143
|
|
|
141
144
|
// Create new media element with appropriate type
|
|
142
145
|
const mediaElement = document.createElement(elementType);
|
|
143
|
-
|
|
146
|
+
// Respect configured preload (playlists should behave like single videos even with deferLoad)
|
|
147
|
+
const preloadValue = preservedPlayerOptions.preload || 'metadata';
|
|
148
|
+
mediaElement.setAttribute('preload', preloadValue);
|
|
144
149
|
|
|
145
150
|
// For video elements with local media, set poster
|
|
146
151
|
if (elementType === 'video' && track.poster &&
|
|
@@ -188,6 +193,8 @@ export class PlaylistManager {
|
|
|
188
193
|
audioDescriptionDuration: track.audioDescriptionDuration || null,
|
|
189
194
|
signLanguageSrc: track.signLanguageSrc || null
|
|
190
195
|
};
|
|
196
|
+
// Merge back preserved options (so deferLoad/preload/etc remain active)
|
|
197
|
+
Object.assign(playerOptions, preservedPlayerOptions);
|
|
191
198
|
|
|
192
199
|
this.player = new this.PlayerClass(mediaElement, playerOptions);
|
|
193
200
|
|
|
@@ -425,8 +432,11 @@ export class PlaylistManager {
|
|
|
425
432
|
if (this.options.autoPlayFirst) {
|
|
426
433
|
this.play(0);
|
|
427
434
|
} else {
|
|
428
|
-
//
|
|
429
|
-
|
|
435
|
+
// Behave like a single video: load the first track (metadata/manifest)
|
|
436
|
+
// but do not start playback.
|
|
437
|
+
void this.loadTrack(0).catch(() => {
|
|
438
|
+
// ignore
|
|
439
|
+
});
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
442
|
|
|
@@ -436,6 +446,9 @@ export class PlaylistManager {
|
|
|
436
446
|
|
|
437
447
|
/**
|
|
438
448
|
* Load a track without playing
|
|
449
|
+
* This is the playlist equivalent of a "single video initialized but not started yet":
|
|
450
|
+
* it updates UI selection and loads the media into the player so metadata/manifests
|
|
451
|
+
* and feature managers can be ready, but it does not start playback.
|
|
439
452
|
* @param {number} index - Track index
|
|
440
453
|
*/
|
|
441
454
|
async loadTrack(index) {
|
|
@@ -446,12 +459,13 @@ export class PlaylistManager {
|
|
|
446
459
|
|
|
447
460
|
const track = this.tracks[index];
|
|
448
461
|
|
|
462
|
+
// Always update UI immediately (poster, buttons, duration, etc.).
|
|
463
|
+
// Note: this is UI-only; actual media loading is performed by player.load() below.
|
|
464
|
+
this.selectTrack(index);
|
|
465
|
+
|
|
449
466
|
// Set guard flag to prevent cascade of next() calls during track change
|
|
450
467
|
this.isChangingTrack = true;
|
|
451
468
|
|
|
452
|
-
// Update current index
|
|
453
|
-
this.currentIndex = index;
|
|
454
|
-
|
|
455
469
|
// Check if we should recreate the player for this track type
|
|
456
470
|
if (this.options.recreatePlayers && this.hostElement && this.PlayerClass) {
|
|
457
471
|
const currentMediaType = this.player ?
|
|
@@ -462,9 +476,8 @@ export class PlaylistManager {
|
|
|
462
476
|
// Recreate if element type is different
|
|
463
477
|
if (currentMediaType !== newElementType) {
|
|
464
478
|
await this.recreatePlayerForTrack(track, false);
|
|
465
|
-
//
|
|
466
|
-
this.
|
|
467
|
-
this.updatePlaylistUI();
|
|
479
|
+
// Re-apply selection to the newly created player (poster/tracks/buttons)
|
|
480
|
+
this.selectTrack(index);
|
|
468
481
|
|
|
469
482
|
// Emit event
|
|
470
483
|
this.player.emit('playlisttrackchange', {
|
|
@@ -482,18 +495,25 @@ export class PlaylistManager {
|
|
|
482
495
|
}
|
|
483
496
|
|
|
484
497
|
// Load track into player (normal path)
|
|
485
|
-
this.player.load({
|
|
498
|
+
const loadPromise = this.player.load({
|
|
486
499
|
src: track.src,
|
|
487
500
|
type: track.type,
|
|
488
501
|
poster: track.poster,
|
|
489
502
|
tracks: track.tracks || [],
|
|
490
503
|
audioDescriptionSrc: track.audioDescriptionSrc || null,
|
|
491
|
-
signLanguageSrc: track.signLanguageSrc || null
|
|
504
|
+
signLanguageSrc: track.signLanguageSrc || null,
|
|
505
|
+
signLanguageSources: track.signLanguageSources || {}
|
|
492
506
|
});
|
|
493
|
-
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
this.
|
|
507
|
+
|
|
508
|
+
// For playlist UX parity with single videos: fetch metadata/manifest now,
|
|
509
|
+
// but do not start playback.
|
|
510
|
+
if (this.player?.options?.deferLoad && typeof this.player.ensureLoaded === 'function') {
|
|
511
|
+
Promise.resolve(loadPromise)
|
|
512
|
+
.then(() => this.player?.ensureLoaded?.())
|
|
513
|
+
.catch(() => {
|
|
514
|
+
// ignore
|
|
515
|
+
});
|
|
516
|
+
}
|
|
497
517
|
|
|
498
518
|
// Emit event
|
|
499
519
|
this.player.emit('playlisttrackchange', {
|
|
@@ -507,6 +527,133 @@ export class PlaylistManager {
|
|
|
507
527
|
this.isChangingTrack = false;
|
|
508
528
|
}, 150);
|
|
509
529
|
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Select a track (UI/selection only; does NOT set the media src / does NOT initialize renderer)
|
|
533
|
+
*
|
|
534
|
+
* In "B always" playlist mode, you typically want `loadTrack()` on selection so the
|
|
535
|
+
* selected item behaves like a single video (metadata/manifest loaded, features ready)
|
|
536
|
+
* without auto-playing.
|
|
537
|
+
* @param {number} index - Track index
|
|
538
|
+
*/
|
|
539
|
+
selectTrack(index) {
|
|
540
|
+
if (index < 0 || index >= this.tracks.length) {
|
|
541
|
+
console.warn('VidPly Playlist: Invalid track index', index);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const track = this.tracks[index];
|
|
546
|
+
this.currentIndex = index;
|
|
547
|
+
|
|
548
|
+
// Apply per-track metadata without touching the media source.
|
|
549
|
+
// This ensures poster + feature buttons (chapters/captions/transcript/sign-language)
|
|
550
|
+
// can be updated instantly even before any media network activity happens.
|
|
551
|
+
try {
|
|
552
|
+
// Poster for video element
|
|
553
|
+
if (this.player?.element?.tagName === 'VIDEO') {
|
|
554
|
+
if (track.poster) {
|
|
555
|
+
const posterUrl = typeof this.player.resolvePosterPath === 'function'
|
|
556
|
+
? this.player.resolvePosterPath(track.poster)
|
|
557
|
+
: track.poster;
|
|
558
|
+
this.player.element.poster = posterUrl;
|
|
559
|
+
this.player.applyPosterAspectRatio?.(posterUrl);
|
|
560
|
+
} else {
|
|
561
|
+
this.player.element.removeAttribute('poster');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Update sign language / audio description sources (used for button visibility)
|
|
566
|
+
this.player.audioDescriptionSrc = track.audioDescriptionSrc || null;
|
|
567
|
+
this.player.signLanguageSrc = track.signLanguageSrc || null;
|
|
568
|
+
this.player.signLanguageSources = track.signLanguageSources || {};
|
|
569
|
+
|
|
570
|
+
// Fill duration early for UI (progress/time display) without loading media
|
|
571
|
+
if (track.duration && Number(track.duration) > 0) {
|
|
572
|
+
this.player.state.duration = Number(track.duration);
|
|
573
|
+
}
|
|
574
|
+
// Also sync feature managers (they keep their own copy of sources)
|
|
575
|
+
if (this.player.audioDescriptionManager) {
|
|
576
|
+
this.player.audioDescriptionManager.src = track.audioDescriptionSrc || null;
|
|
577
|
+
// Remember original (non-described) source for switching back later
|
|
578
|
+
this.player.audioDescriptionManager.originalSource = track.src || this.player.originalSrc || null;
|
|
579
|
+
}
|
|
580
|
+
if (this.player.signLanguageManager) {
|
|
581
|
+
this.player.signLanguageManager.src = track.signLanguageSrc || null;
|
|
582
|
+
this.player.signLanguageManager.sources = track.signLanguageSources || {};
|
|
583
|
+
this.player.signLanguageManager.currentLanguage = null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// For audio description switching, remember original source even before first play
|
|
587
|
+
if (track.src && !this.player.originalSrc) {
|
|
588
|
+
this.player.originalSrc = track.src;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Replace <track> elements so captions/chapters/transcript can be detected/loaded
|
|
592
|
+
const existing = Array.from(this.player.element.querySelectorAll('track'));
|
|
593
|
+
existing.forEach(t => t.remove());
|
|
594
|
+
|
|
595
|
+
if (Array.isArray(track.tracks)) {
|
|
596
|
+
track.tracks.forEach(tc => {
|
|
597
|
+
if (!tc?.src) return;
|
|
598
|
+
const el = document.createElement('track');
|
|
599
|
+
el.src = tc.src;
|
|
600
|
+
el.kind = tc.kind || 'captions';
|
|
601
|
+
el.srclang = tc.srclang || 'en';
|
|
602
|
+
el.label = tc.label || tc.srclang || 'Track';
|
|
603
|
+
if (tc.default) el.default = true;
|
|
604
|
+
if (tc.describedSrc) {
|
|
605
|
+
el.setAttribute('data-desc-src', tc.describedSrc);
|
|
606
|
+
}
|
|
607
|
+
this.player.element.appendChild(el);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (typeof this.player.invalidateTrackCache === 'function') {
|
|
612
|
+
this.player.invalidateTrackCache();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Re-scan described-track metadata for AudioDescriptionManager
|
|
616
|
+
if (this.player.audioDescriptionManager && typeof this.player.audioDescriptionManager.initFromSourceElements === 'function') {
|
|
617
|
+
try {
|
|
618
|
+
this.player.audioDescriptionManager.captionTracks = [];
|
|
619
|
+
this.player.audioDescriptionManager.initFromSourceElements(this.player.sourceElements, this.player.trackElements);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
// ignore
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Refresh caption/transcript managers so menus reflect newly injected <track> elements
|
|
626
|
+
// (important when we defer MP4/MP3 loading but still want VTT-based UI to work).
|
|
627
|
+
if (this.player.captionManager && typeof this.player.captionManager.loadTracks === 'function') {
|
|
628
|
+
try {
|
|
629
|
+
this.player.captionManager.tracks = [];
|
|
630
|
+
this.player.captionManager.currentTrack = null;
|
|
631
|
+
this.player.captionManager.loadTracks();
|
|
632
|
+
} catch (e) {
|
|
633
|
+
// ignore
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// TranscriptManager reads from TextTracks too; it will be correct after media starts.
|
|
638
|
+
// For now, we just ensure control bar is rebuilt so the button is present.
|
|
639
|
+
|
|
640
|
+
// Rebuild controls so feature buttons appear immediately
|
|
641
|
+
if (typeof this.player.updateControlBar === 'function') {
|
|
642
|
+
this.player.updateControlBar();
|
|
643
|
+
}
|
|
644
|
+
} catch (e) {
|
|
645
|
+
// ignore preview errors; selection should still work
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.updateTrackInfo(track);
|
|
649
|
+
this.updatePlaylistUI();
|
|
650
|
+
|
|
651
|
+
this.player.emit('playlisttrackselect', {
|
|
652
|
+
index,
|
|
653
|
+
item: track,
|
|
654
|
+
total: this.tracks.length
|
|
655
|
+
});
|
|
656
|
+
}
|
|
510
657
|
|
|
511
658
|
/**
|
|
512
659
|
* Play a specific track
|
|
@@ -557,13 +704,24 @@ export class PlaylistManager {
|
|
|
557
704
|
}
|
|
558
705
|
|
|
559
706
|
// Load track into player (normal path)
|
|
707
|
+
// If audio description was toggled before the first play, load the described source directly.
|
|
708
|
+
let srcToLoad = track.src;
|
|
709
|
+
if (this.player?.audioDescriptionManager?.desiredState && track.audioDescriptionSrc) {
|
|
710
|
+
// Preserve original for later toggling back
|
|
711
|
+
this.player.originalSrc = track.src;
|
|
712
|
+
this.player.audioDescriptionManager.originalSource = track.src;
|
|
713
|
+
this.player.audioDescriptionManager.src = track.audioDescriptionSrc;
|
|
714
|
+
srcToLoad = track.audioDescriptionSrc;
|
|
715
|
+
}
|
|
716
|
+
|
|
560
717
|
this.player.load({
|
|
561
|
-
src:
|
|
718
|
+
src: srcToLoad,
|
|
562
719
|
type: track.type,
|
|
563
720
|
poster: track.poster,
|
|
564
721
|
tracks: track.tracks || [],
|
|
565
722
|
audioDescriptionSrc: track.audioDescriptionSrc || null,
|
|
566
|
-
signLanguageSrc: track.signLanguageSrc || null
|
|
723
|
+
signLanguageSrc: track.signLanguageSrc || null,
|
|
724
|
+
signLanguageSources: track.signLanguageSources || {}
|
|
567
725
|
});
|
|
568
726
|
|
|
569
727
|
// Update UI
|
package/src/i18n/languages/de.js
CHANGED
|
@@ -45,6 +45,9 @@ export const de = {
|
|
|
45
45
|
signLanguageVideo: 'Gebärdensprache-Video',
|
|
46
46
|
closeSignLanguage: 'Gebärdensprache-Video schließen',
|
|
47
47
|
signLanguageSettings: 'Gebärdensprache-Einstellungen',
|
|
48
|
+
startPlaybackFirst: 'Bitte starten Sie die Wiedergabe zuerst.',
|
|
49
|
+
startPlaybackForAudioDescription: 'Bitte starten Sie die Wiedergabe zuerst, um die Audiodeskription zu nutzen.',
|
|
50
|
+
startPlaybackForSignLanguage: 'Bitte starten Sie die Wiedergabe zuerst, um das Gebärdensprache-Video zu nutzen.',
|
|
48
51
|
noChapters: 'Keine Kapitel verfügbar',
|
|
49
52
|
noCaptions: 'Keine Untertitel verfügbar',
|
|
50
53
|
auto: 'Automatisch',
|
package/src/i18n/languages/en.js
CHANGED
|
@@ -45,6 +45,9 @@ export const en = {
|
|
|
45
45
|
signLanguageVideo: 'Sign Language Video',
|
|
46
46
|
closeSignLanguage: 'Close sign language video',
|
|
47
47
|
signLanguageSettings: 'Sign language settings',
|
|
48
|
+
startPlaybackFirst: 'Please start playback first.',
|
|
49
|
+
startPlaybackForAudioDescription: 'Please start playback first to use audio description.',
|
|
50
|
+
startPlaybackForSignLanguage: 'Please start playback first to use sign language video.',
|
|
48
51
|
noChapters: 'No chapters available',
|
|
49
52
|
noCaptions: 'No captions available',
|
|
50
53
|
auto: 'Auto',
|