vidply 1.0.32 → 1.0.34
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-YGWCAICA.js} +49 -4
- package/dist/dev/vidply.HLSRenderer-YGWCAICA.js.map +7 -0
- package/dist/dev/{vidply.HTML5Renderer-FXBZQL6Y.js → vidply.HTML5Renderer-PMNFHAKW.js} +3 -3
- package/dist/dev/{vidply.SoundCloudRenderer-CD7VJKNS.js → vidply.SoundCloudRenderer-RIA3QKP3.js} +2 -2
- package/dist/dev/{vidply.TranscriptManager-T677KF4N.js → vidply.TranscriptManager-T3BVTZHZ.js} +3 -3
- package/dist/dev/{vidply.VimeoRenderer-VPH4RNES.js → vidply.VimeoRenderer-DY2FG7LZ.js} +2 -2
- package/dist/dev/{vidply.YouTubeRenderer-6MGKEFTZ.js → vidply.YouTubeRenderer-EVXXE34A.js} +2 -2
- package/dist/dev/{vidply.chunk-GS2JX5RQ.js → vidply.chunk-74NJTDQI.js} +9 -6
- package/dist/dev/vidply.chunk-74NJTDQI.js.map +7 -0
- package/dist/dev/{vidply.chunk-W2LSBD6Y.js → vidply.chunk-IIN4G4UQ.js} +34 -4
- package/dist/dev/vidply.chunk-IIN4G4UQ.js.map +7 -0
- package/dist/dev/{vidply.de-SNL6AJ4D.js → vidply.de-YBEYEXBL.js} +5 -2
- package/dist/dev/vidply.de-YBEYEXBL.js.map +7 -0
- package/dist/dev/{vidply.es-2QCQKZ4U.js → vidply.es-QA4YSA5S.js} +2 -2
- package/dist/dev/vidply.esm.js +389 -82
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-FJAZRL4L.js → vidply.fr-LAM3XJZI.js} +2 -2
- package/dist/dev/{vidply.ja-2XQOW53T.js → vidply.ja-FTBFZD66.js} +2 -2
- package/dist/legacy/vidply.js +482 -79
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +15 -15
- package/dist/prod/vidply.HLSRenderer-D2KTBEEI.min.js +6 -0
- package/dist/prod/{vidply.HTML5Renderer-KKW3OLHM.min.js → vidply.HTML5Renderer-ZSV6PDOH.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-MOR2CUFH.min.js → vidply.SoundCloudRenderer-BFV5SSIU.min.js} +1 -1
- package/dist/prod/{vidply.TranscriptManager-WFZSW6NR.min.js → vidply.TranscriptManager-GPAOXEK4.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-3HBMM2WR.min.js → vidply.VimeoRenderer-UQWHQ4LC.min.js} +1 -1
- package/dist/prod/{vidply.YouTubeRenderer-MFC2GMAC.min.js → vidply.YouTubeRenderer-K7A57ICA.min.js} +1 -1
- package/dist/prod/vidply.chunk-OM7DNW5P.min.js +6 -0
- package/dist/prod/vidply.chunk-SQVOYVKH.min.js +6 -0
- package/dist/prod/vidply.de-WCUZUF3T.min.js +6 -0
- package/dist/prod/{vidply.es-3IJCQLJ7.min.js → vidply.es-54CIIDMO.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +5 -5
- package/dist/prod/{vidply.fr-NC4VEAPH.min.js → vidply.fr-7FYGFFK2.min.js} +1 -1
- package/dist/prod/{vidply.ja-4ZC6ZQLV.min.js → vidply.ja-E4UTAURP.min.js} +1 -1
- package/dist/vidply.css +1 -1
- package/dist/vidply.esm.min.meta.json +45 -45
- package/dist/vidply.min.css +1 -1
- package/package.json +1 -1
- package/src/controls/ControlBar.js +104 -70
- package/src/core/Player.js +217 -3
- package/src/features/PlaylistManager.js +206 -36
- 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-PMNFHAKW.js.map} +0 -0
- /package/dist/dev/{vidply.SoundCloudRenderer-CD7VJKNS.js.map → vidply.SoundCloudRenderer-RIA3QKP3.js.map} +0 -0
- /package/dist/dev/{vidply.TranscriptManager-T677KF4N.js.map → vidply.TranscriptManager-T3BVTZHZ.js.map} +0 -0
- /package/dist/dev/{vidply.VimeoRenderer-VPH4RNES.js.map → vidply.VimeoRenderer-DY2FG7LZ.js.map} +0 -0
- /package/dist/dev/{vidply.YouTubeRenderer-6MGKEFTZ.js.map → vidply.YouTubeRenderer-EVXXE34A.js.map} +0 -0
- /package/dist/dev/{vidply.es-2QCQKZ4U.js.map → vidply.es-QA4YSA5S.js.map} +0 -0
- /package/dist/dev/{vidply.fr-FJAZRL4L.js.map → vidply.fr-LAM3XJZI.js.map} +0 -0
- /package/dist/dev/{vidply.ja-2XQOW53T.js.map → vidply.ja-FTBFZD66.js.map} +0 -0
|
@@ -27,6 +27,10 @@ export class ControlBar {
|
|
|
27
27
|
init() {
|
|
28
28
|
this.createElement();
|
|
29
29
|
this.createControls();
|
|
30
|
+
// Ensure time UI reflects any prefilled state (e.g. initialDuration)
|
|
31
|
+
// even when media metadata is deferred and 'loadedmetadata' won't fire yet.
|
|
32
|
+
this.updateDuration();
|
|
33
|
+
this.updateProgress();
|
|
30
34
|
this.attachEvents();
|
|
31
35
|
this.setupAutoHide();
|
|
32
36
|
this.setupOverflowDetection();
|
|
@@ -846,22 +850,44 @@ export class ControlBar {
|
|
|
846
850
|
|
|
847
851
|
// Helper methods to check for available features
|
|
848
852
|
hasChapterTracks() {
|
|
853
|
+
// 1) Prefer already-loaded TextTracks (fast + accurate)
|
|
849
854
|
const textTracks = this.player.element.textTracks;
|
|
850
855
|
for (let i = 0; i < textTracks.length; i++) {
|
|
851
|
-
if (textTracks[i].kind === 'chapters')
|
|
852
|
-
return true;
|
|
856
|
+
if (textTracks[i].kind === 'chapters') return true;
|
|
853
857
|
}
|
|
858
|
+
|
|
859
|
+
// 2) Fallback to DOM <track> elements (works before tracks are fully loaded)
|
|
860
|
+
const trackEls = Array.from(this.player.element.querySelectorAll('track[kind="chapters"]'));
|
|
861
|
+
if (trackEls.length > 0) return true;
|
|
862
|
+
|
|
863
|
+
// 3) Playlist metadata fallback (works even when we intentionally defer loading)
|
|
864
|
+
const current = this.player.playlistManager?.getCurrentTrack?.();
|
|
865
|
+
if (current?.tracks && Array.isArray(current.tracks)) {
|
|
866
|
+
return current.tracks.some(t => t?.kind === 'chapters');
|
|
854
867
|
}
|
|
868
|
+
|
|
855
869
|
return false;
|
|
856
870
|
}
|
|
857
871
|
|
|
858
872
|
hasCaptionTracks() {
|
|
873
|
+
// 1) Prefer already-loaded TextTracks
|
|
859
874
|
const textTracks = this.player.element.textTracks;
|
|
860
875
|
for (let i = 0; i < textTracks.length; i++) {
|
|
861
|
-
if (textTracks[i].kind === 'captions' || textTracks[i].kind === 'subtitles')
|
|
876
|
+
if (textTracks[i].kind === 'captions' || textTracks[i].kind === 'subtitles') return true;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// 2) Fallback to DOM <track> elements
|
|
880
|
+
const trackEls = Array.from(this.player.element.querySelectorAll('track'));
|
|
881
|
+
if (trackEls.some(el => (el.getAttribute('kind') === 'captions' || el.getAttribute('kind') === 'subtitles'))) {
|
|
862
882
|
return true;
|
|
863
883
|
}
|
|
884
|
+
|
|
885
|
+
// 3) Playlist metadata fallback
|
|
886
|
+
const current = this.player.playlistManager?.getCurrentTrack?.();
|
|
887
|
+
if (current?.tracks && Array.isArray(current.tracks)) {
|
|
888
|
+
return current.tracks.some(t => t?.kind === 'captions' || t?.kind === 'subtitles');
|
|
864
889
|
}
|
|
890
|
+
|
|
865
891
|
return false;
|
|
866
892
|
}
|
|
867
893
|
|
|
@@ -963,6 +989,8 @@ export class ControlBar {
|
|
|
963
989
|
this.currentPreviewTime = null;
|
|
964
990
|
this.previewThumbnailTimeout = null;
|
|
965
991
|
this.previewSupported = false;
|
|
992
|
+
this.previewVideoReady = false;
|
|
993
|
+
this.previewVideoInitialized = false;
|
|
966
994
|
|
|
967
995
|
// Check if preview is supported (HTML5 video only)
|
|
968
996
|
// Check if element is a video
|
|
@@ -972,80 +1000,75 @@ export class ControlBar {
|
|
|
972
1000
|
return;
|
|
973
1001
|
}
|
|
974
1002
|
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
|
|
1003
|
+
// IMPORTANT: do NOT create/load the preview video until the user has started playback at least once.
|
|
1004
|
+
// Otherwise we'd trigger heavy MP4 network traffic just by hovering the progress bar.
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Lazily create the hidden preview video (only after playback started once)
|
|
1009
|
+
*/
|
|
1010
|
+
ensurePreviewVideoInitialized() {
|
|
1011
|
+
if (this.previewVideoInitialized) return;
|
|
1012
|
+
if (!this.player?.state?.hasStartedPlayback) return;
|
|
1013
|
+
|
|
978
1014
|
const renderer = this.player.renderer;
|
|
979
1015
|
const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === 'VIDEO';
|
|
980
|
-
|
|
981
|
-
// Check if it's HTML5Renderer by checking:
|
|
982
|
-
// 1. Has media property that is a video element
|
|
983
|
-
// 2. Media is the same as player.element (HTML5Renderer sets this.media = player.element)
|
|
984
|
-
// 3. Doesn't have hls property (HLSRenderer has hls property)
|
|
985
|
-
// 4. Has seek method (all renderers have this, but combined with above checks it's reliable)
|
|
986
|
-
const isHTML5Renderer = hasVideoMedia &&
|
|
1016
|
+
const isHTML5Renderer = hasVideoMedia &&
|
|
987
1017
|
renderer.media === this.player.element &&
|
|
988
1018
|
!renderer.hls &&
|
|
989
1019
|
typeof renderer.seek === 'function';
|
|
990
|
-
|
|
1020
|
+
|
|
991
1021
|
this.previewSupported = isHTML5Renderer && hasVideoMedia;
|
|
1022
|
+
if (!this.previewSupported) return;
|
|
992
1023
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
this.previewVideo.style.width = '1px';
|
|
1002
|
-
this.previewVideo.style.height = '1px';
|
|
1003
|
-
this.previewVideo.style.top = '-9999px';
|
|
1004
|
-
|
|
1005
|
-
// Copy source and attributes from main video
|
|
1006
|
-
const mainVideo = renderer.media || this.player.element;
|
|
1007
|
-
let videoSrc = null;
|
|
1008
|
-
|
|
1009
|
-
if (mainVideo.src) {
|
|
1010
|
-
videoSrc = mainVideo.src;
|
|
1011
|
-
} else {
|
|
1012
|
-
const source = mainVideo.querySelector('source');
|
|
1013
|
-
if (source) {
|
|
1014
|
-
videoSrc = source.src;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
if (!videoSrc) {
|
|
1019
|
-
this.player.log('No video source found for preview', 'warn');
|
|
1020
|
-
this.previewSupported = false;
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Copy crossOrigin if set (important for CORS)
|
|
1025
|
-
if (mainVideo.crossOrigin) {
|
|
1026
|
-
this.previewVideo.crossOrigin = mainVideo.crossOrigin;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Handle errors gracefully
|
|
1030
|
-
this.previewVideo.addEventListener('error', (e) => {
|
|
1031
|
-
this.player.log('Preview video failed to load:', e, 'warn');
|
|
1032
|
-
this.previewSupported = false;
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
// Wait for metadata to be loaded before using
|
|
1036
|
-
this.previewVideo.addEventListener('loadedmetadata', () => {
|
|
1037
|
-
this.previewVideoReady = true;
|
|
1038
|
-
}, { once: true });
|
|
1039
|
-
|
|
1040
|
-
// Append to player container (hidden) BEFORE setting src
|
|
1041
|
-
if (this.player.container) {
|
|
1042
|
-
this.player.container.appendChild(this.previewVideo);
|
|
1024
|
+
const mainVideo = renderer.media || this.player.element;
|
|
1025
|
+
let videoSrc = null;
|
|
1026
|
+
if (mainVideo.src) {
|
|
1027
|
+
videoSrc = mainVideo.src;
|
|
1028
|
+
} else {
|
|
1029
|
+
const source = mainVideo.querySelector('source');
|
|
1030
|
+
if (source) {
|
|
1031
|
+
videoSrc = source.src;
|
|
1043
1032
|
}
|
|
1044
|
-
|
|
1045
|
-
// Set source after appending to DOM
|
|
1046
|
-
this.previewVideo.src = videoSrc;
|
|
1047
|
-
this.previewVideoReady = false;
|
|
1048
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
if (!videoSrc) {
|
|
1036
|
+
this.player.log('No video source found for preview', 'warn');
|
|
1037
|
+
this.previewSupported = false;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Create a hidden video element for capturing frames
|
|
1042
|
+
this.previewVideo = document.createElement('video');
|
|
1043
|
+
this.previewVideo.muted = true;
|
|
1044
|
+
this.previewVideo.preload = 'auto'; // Need more than metadata to capture frames
|
|
1045
|
+
this.previewVideo.playsInline = true;
|
|
1046
|
+
this.previewVideo.style.position = 'absolute';
|
|
1047
|
+
this.previewVideo.style.visibility = 'hidden';
|
|
1048
|
+
this.previewVideo.style.width = '1px';
|
|
1049
|
+
this.previewVideo.style.height = '1px';
|
|
1050
|
+
this.previewVideo.style.top = '-9999px';
|
|
1051
|
+
|
|
1052
|
+
if (mainVideo.crossOrigin) {
|
|
1053
|
+
this.previewVideo.crossOrigin = mainVideo.crossOrigin;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
this.previewVideo.addEventListener('error', (e) => {
|
|
1057
|
+
this.player.log('Preview video failed to load:', e, 'warn');
|
|
1058
|
+
this.previewSupported = false;
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
this.previewVideo.addEventListener('loadedmetadata', () => {
|
|
1062
|
+
this.previewVideoReady = true;
|
|
1063
|
+
}, { once: true });
|
|
1064
|
+
|
|
1065
|
+
if (this.player.container) {
|
|
1066
|
+
this.player.container.appendChild(this.previewVideo);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
this.previewVideo.src = videoSrc;
|
|
1070
|
+
this.previewVideoReady = false;
|
|
1071
|
+
this.previewVideoInitialized = true;
|
|
1049
1072
|
}
|
|
1050
1073
|
|
|
1051
1074
|
/**
|
|
@@ -1218,10 +1241,21 @@ export class ControlBar {
|
|
|
1218
1241
|
// Update tooltip position
|
|
1219
1242
|
this.controls.progressTooltip.style.left = `${left}px`;
|
|
1220
1243
|
this.controls.progressTooltip.style.display = 'block';
|
|
1221
|
-
|
|
1222
|
-
//
|
|
1244
|
+
|
|
1245
|
+
// Only show preview thumbnails after the user has started playback at least once.
|
|
1246
|
+
// Before that, show just the timestamp (no empty preview box).
|
|
1247
|
+
if (!this.player?.state?.hasStartedPlayback) {
|
|
1248
|
+
if (this.controls.progressPreview) {
|
|
1249
|
+
this.controls.progressPreview.style.display = 'none';
|
|
1250
|
+
}
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
this.ensurePreviewVideoInitialized();
|
|
1223
1255
|
if (this.previewSupported) {
|
|
1224
1256
|
this.updatePreviewThumbnail(time);
|
|
1257
|
+
} else if (this.controls.progressPreview) {
|
|
1258
|
+
this.controls.progressPreview.style.display = 'none';
|
|
1225
1259
|
}
|
|
1226
1260
|
}
|
|
1227
1261
|
});
|
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
|
|