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.
Files changed (63) hide show
  1. package/README.md +7 -7
  2. package/dist/dev/{vidply.HLSRenderer-5MJZR4D2.js → vidply.HLSRenderer-YGWCAICA.js} +49 -4
  3. package/dist/dev/vidply.HLSRenderer-YGWCAICA.js.map +7 -0
  4. package/dist/dev/{vidply.HTML5Renderer-FXBZQL6Y.js → vidply.HTML5Renderer-PMNFHAKW.js} +3 -3
  5. package/dist/dev/{vidply.SoundCloudRenderer-CD7VJKNS.js → vidply.SoundCloudRenderer-RIA3QKP3.js} +2 -2
  6. package/dist/dev/{vidply.TranscriptManager-T677KF4N.js → vidply.TranscriptManager-T3BVTZHZ.js} +3 -3
  7. package/dist/dev/{vidply.VimeoRenderer-VPH4RNES.js → vidply.VimeoRenderer-DY2FG7LZ.js} +2 -2
  8. package/dist/dev/{vidply.YouTubeRenderer-6MGKEFTZ.js → vidply.YouTubeRenderer-EVXXE34A.js} +2 -2
  9. package/dist/dev/{vidply.chunk-GS2JX5RQ.js → vidply.chunk-74NJTDQI.js} +9 -6
  10. package/dist/dev/vidply.chunk-74NJTDQI.js.map +7 -0
  11. package/dist/dev/{vidply.chunk-W2LSBD6Y.js → vidply.chunk-IIN4G4UQ.js} +34 -4
  12. package/dist/dev/vidply.chunk-IIN4G4UQ.js.map +7 -0
  13. package/dist/dev/{vidply.de-SNL6AJ4D.js → vidply.de-YBEYEXBL.js} +5 -2
  14. package/dist/dev/vidply.de-YBEYEXBL.js.map +7 -0
  15. package/dist/dev/{vidply.es-2QCQKZ4U.js → vidply.es-QA4YSA5S.js} +2 -2
  16. package/dist/dev/vidply.esm.js +389 -82
  17. package/dist/dev/vidply.esm.js.map +2 -2
  18. package/dist/dev/{vidply.fr-FJAZRL4L.js → vidply.fr-LAM3XJZI.js} +2 -2
  19. package/dist/dev/{vidply.ja-2XQOW53T.js → vidply.ja-FTBFZD66.js} +2 -2
  20. package/dist/legacy/vidply.js +482 -79
  21. package/dist/legacy/vidply.js.map +3 -3
  22. package/dist/legacy/vidply.min.js +2 -2
  23. package/dist/legacy/vidply.min.meta.json +15 -15
  24. package/dist/prod/vidply.HLSRenderer-D2KTBEEI.min.js +6 -0
  25. package/dist/prod/{vidply.HTML5Renderer-KKW3OLHM.min.js → vidply.HTML5Renderer-ZSV6PDOH.min.js} +2 -2
  26. package/dist/prod/{vidply.SoundCloudRenderer-MOR2CUFH.min.js → vidply.SoundCloudRenderer-BFV5SSIU.min.js} +1 -1
  27. package/dist/prod/{vidply.TranscriptManager-WFZSW6NR.min.js → vidply.TranscriptManager-GPAOXEK4.min.js} +2 -2
  28. package/dist/prod/{vidply.VimeoRenderer-3HBMM2WR.min.js → vidply.VimeoRenderer-UQWHQ4LC.min.js} +1 -1
  29. package/dist/prod/{vidply.YouTubeRenderer-MFC2GMAC.min.js → vidply.YouTubeRenderer-K7A57ICA.min.js} +1 -1
  30. package/dist/prod/vidply.chunk-OM7DNW5P.min.js +6 -0
  31. package/dist/prod/vidply.chunk-SQVOYVKH.min.js +6 -0
  32. package/dist/prod/vidply.de-WCUZUF3T.min.js +6 -0
  33. package/dist/prod/{vidply.es-3IJCQLJ7.min.js → vidply.es-54CIIDMO.min.js} +1 -1
  34. package/dist/prod/vidply.esm.min.js +5 -5
  35. package/dist/prod/{vidply.fr-NC4VEAPH.min.js → vidply.fr-7FYGFFK2.min.js} +1 -1
  36. package/dist/prod/{vidply.ja-4ZC6ZQLV.min.js → vidply.ja-E4UTAURP.min.js} +1 -1
  37. package/dist/vidply.css +1 -1
  38. package/dist/vidply.esm.min.meta.json +45 -45
  39. package/dist/vidply.min.css +1 -1
  40. package/package.json +1 -1
  41. package/src/controls/ControlBar.js +104 -70
  42. package/src/core/Player.js +217 -3
  43. package/src/features/PlaylistManager.js +206 -36
  44. package/src/i18n/languages/de.js +3 -0
  45. package/src/i18n/languages/en.js +3 -0
  46. package/src/renderers/HLSRenderer.js +60 -1
  47. package/src/renderers/HTML5Renderer.js +43 -5
  48. package/dist/dev/vidply.HLSRenderer-5MJZR4D2.js.map +0 -7
  49. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +0 -7
  50. package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +0 -7
  51. package/dist/dev/vidply.de-SNL6AJ4D.js.map +0 -7
  52. package/dist/prod/vidply.HLSRenderer-VWNJD2CB.min.js +0 -6
  53. package/dist/prod/vidply.chunk-34RH2THY.min.js +0 -6
  54. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +0 -6
  55. package/dist/prod/vidply.de-FR3XX54P.min.js +0 -6
  56. /package/dist/dev/{vidply.HTML5Renderer-FXBZQL6Y.js.map → vidply.HTML5Renderer-PMNFHAKW.js.map} +0 -0
  57. /package/dist/dev/{vidply.SoundCloudRenderer-CD7VJKNS.js.map → vidply.SoundCloudRenderer-RIA3QKP3.js.map} +0 -0
  58. /package/dist/dev/{vidply.TranscriptManager-T677KF4N.js.map → vidply.TranscriptManager-T3BVTZHZ.js.map} +0 -0
  59. /package/dist/dev/{vidply.VimeoRenderer-VPH4RNES.js.map → vidply.VimeoRenderer-DY2FG7LZ.js.map} +0 -0
  60. /package/dist/dev/{vidply.YouTubeRenderer-6MGKEFTZ.js.map → vidply.YouTubeRenderer-EVXXE34A.js.map} +0 -0
  61. /package/dist/dev/{vidply.es-2QCQKZ4U.js.map → vidply.es-QA4YSA5S.js.map} +0 -0
  62. /package/dist/dev/{vidply.fr-FJAZRL4L.js.map → vidply.fr-LAM3XJZI.js.map} +0 -0
  63. /package/dist/dev/{vidply.ja-2XQOW53T.js.map → vidply.ja-FTBFZD66.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Universal, Accessible Video Player
3
- * (c) 2025 Matthias Peltzer
3
+ * (c) 2026 Matthias Peltzer
4
4
  * Released under GPL-2.0-or-later License
5
5
  */
6
6
  (() => {
@@ -296,6 +296,9 @@
296
296
  signLanguageVideo: "Sign Language Video",
297
297
  closeSignLanguage: "Close sign language video",
298
298
  signLanguageSettings: "Sign language settings",
299
+ startPlaybackFirst: "Please start playback first.",
300
+ startPlaybackForAudioDescription: "Please start playback first to use audio description.",
301
+ startPlaybackForSignLanguage: "Please start playback first to use sign language video.",
299
302
  noChapters: "No chapters available",
300
303
  noCaptions: "No captions available",
301
304
  auto: "Auto",
@@ -484,6 +487,9 @@
484
487
  signLanguageVideo: "Gebärdensprache-Video",
485
488
  closeSignLanguage: "Gebärdensprache-Video schließen",
486
489
  signLanguageSettings: "Gebärdensprache-Einstellungen",
490
+ startPlaybackFirst: "Bitte starten Sie die Wiedergabe zuerst.",
491
+ startPlaybackForAudioDescription: "Bitte starten Sie die Wiedergabe zuerst, um die Audiodeskription zu nutzen.",
492
+ startPlaybackForSignLanguage: "Bitte starten Sie die Wiedergabe zuerst, um das Gebärdensprache-Video zu nutzen.",
487
493
  noChapters: "Keine Kapitel verfügbar",
488
494
  noCaptions: "Keine Untertitel verfügbar",
489
495
  auto: "Automatisch",
@@ -1726,13 +1732,18 @@
1726
1732
  constructor(player) {
1727
1733
  this.player = player;
1728
1734
  this.media = player.element;
1735
+ this._didDeferredLoad = false;
1729
1736
  }
1730
1737
  async init() {
1731
1738
  this.media.controls = false;
1732
1739
  this.media.removeAttribute("controls");
1733
1740
  this.attachEvents();
1734
- this.media.preload = this.player.options.preload;
1735
- this.media.load();
1741
+ if (this.player.options.deferLoad) {
1742
+ this.media.preload = this.player.options.preload || "none";
1743
+ } else {
1744
+ this.media.preload = this.player.options.preload;
1745
+ this.media.load();
1746
+ }
1736
1747
  if (this.player.container) {
1737
1748
  this.player.container.classList.remove("vidply-external-controls");
1738
1749
  }
@@ -1839,6 +1850,15 @@
1839
1850
  play() {
1840
1851
  const scrollX = window.scrollX;
1841
1852
  const scrollY = window.scrollY;
1853
+ if (this.player.options.deferLoad && !this._didDeferredLoad) {
1854
+ try {
1855
+ if (this.media.readyState === 0) {
1856
+ this.media.load();
1857
+ }
1858
+ } catch (e) {
1859
+ }
1860
+ this._didDeferredLoad = true;
1861
+ }
1842
1862
  const promise = this.media.play();
1843
1863
  window.scrollTo(scrollX, scrollY);
1844
1864
  if (promise !== void 0) {
@@ -1858,6 +1878,22 @@
1858
1878
  });
1859
1879
  }
1860
1880
  }
1881
+ /**
1882
+ * Ensure the media element has been loaded at least once (metadata/initial state)
1883
+ * without starting playback. Useful for playlists to behave like single videos.
1884
+ */
1885
+ ensureLoaded() {
1886
+ if (!this.player.options.deferLoad || this._didDeferredLoad) {
1887
+ return;
1888
+ }
1889
+ try {
1890
+ if (this.media.readyState === 0) {
1891
+ this.media.load();
1892
+ }
1893
+ } catch (e) {
1894
+ }
1895
+ this._didDeferredLoad = true;
1896
+ }
1861
1897
  pause() {
1862
1898
  this.media.pause();
1863
1899
  }
@@ -4967,6 +5003,8 @@
4967
5003
  this.player = player;
4968
5004
  this.media = player.element;
4969
5005
  this.hls = null;
5006
+ this._hlsSourceLoaded = false;
5007
+ this._pendingSrc = null;
4970
5008
  }
4971
5009
  async init() {
4972
5010
  if (this.canPlayNatively()) {
@@ -5007,6 +5045,8 @@
5007
5045
  }
5008
5046
  this.hls = new window.Hls({
5009
5047
  debug: this.player.options.debug,
5048
+ // When deferLoad is enabled, do not start loading until the first play().
5049
+ autoStartLoad: !this.player.options.deferLoad,
5010
5050
  enableWorker: true,
5011
5051
  lowLatencyMode: false,
5012
5052
  backBufferLength: 90,
@@ -5042,7 +5082,12 @@
5042
5082
  if (!src) {
5043
5083
  throw new Error("No HLS source found");
5044
5084
  }
5045
- this.hls.loadSource(src);
5085
+ if (this.player.options.deferLoad) {
5086
+ this._pendingSrc = src;
5087
+ } else {
5088
+ this.hls.loadSource(src);
5089
+ this._hlsSourceLoaded = true;
5090
+ }
5046
5091
  this.attachHlsEvents();
5047
5092
  this.attachMediaEvents();
5048
5093
  }
@@ -5161,9 +5206,45 @@
5161
5206
  this.player.log("Non-fatal HLS error: " + data.details, "warn");
5162
5207
  }
5163
5208
  }
5209
+ /**
5210
+ * Ensure the HLS manifest/initial loading is started without starting playback.
5211
+ * This makes playlist selection behave more like single-video initialization.
5212
+ */
5213
+ ensureLoaded() {
5214
+ if (!this.player.options.deferLoad) {
5215
+ return;
5216
+ }
5217
+ if (!this.hls) {
5218
+ return;
5219
+ }
5220
+ if (this._hlsSourceLoaded) {
5221
+ return;
5222
+ }
5223
+ const src = this._pendingSrc || this.player._pendingSource || this.player.currentSource;
5224
+ if (!src) {
5225
+ return;
5226
+ }
5227
+ try {
5228
+ this.hls.loadSource(src);
5229
+ this._hlsSourceLoaded = true;
5230
+ this.hls.startLoad();
5231
+ } catch (e) {
5232
+ }
5233
+ }
5164
5234
  play() {
5165
5235
  const scrollX = window.scrollX;
5166
5236
  const scrollY = window.scrollY;
5237
+ if (this.player.options.deferLoad && this.hls && !this._hlsSourceLoaded) {
5238
+ const src = this._pendingSrc || this.player.currentSource;
5239
+ if (src) {
5240
+ try {
5241
+ this.hls.loadSource(src);
5242
+ this.hls.startLoad();
5243
+ this._hlsSourceLoaded = true;
5244
+ } catch (e) {
5245
+ }
5246
+ }
5247
+ }
5167
5248
  const promise = this.media.play();
5168
5249
  window.scrollTo(scrollX, scrollY);
5169
5250
  if (promise !== void 0) {
@@ -5683,6 +5764,8 @@
5683
5764
  init() {
5684
5765
  this.createElement();
5685
5766
  this.createControls();
5767
+ this.updateDuration();
5768
+ this.updateProgress();
5686
5769
  this.attachEvents();
5687
5770
  this.setupAutoHide();
5688
5771
  this.setupOverflowDetection();
@@ -6249,20 +6332,32 @@
6249
6332
  }
6250
6333
  // Helper methods to check for available features
6251
6334
  hasChapterTracks() {
6335
+ var _a, _b;
6252
6336
  const textTracks = this.player.element.textTracks;
6253
6337
  for (let i = 0; i < textTracks.length; i++) {
6254
- if (textTracks[i].kind === "chapters") {
6255
- return true;
6256
- }
6338
+ if (textTracks[i].kind === "chapters") return true;
6339
+ }
6340
+ const trackEls = Array.from(this.player.element.querySelectorAll('track[kind="chapters"]'));
6341
+ if (trackEls.length > 0) return true;
6342
+ const current = (_b = (_a = this.player.playlistManager) == null ? void 0 : _a.getCurrentTrack) == null ? void 0 : _b.call(_a);
6343
+ if ((current == null ? void 0 : current.tracks) && Array.isArray(current.tracks)) {
6344
+ return current.tracks.some((t) => (t == null ? void 0 : t.kind) === "chapters");
6257
6345
  }
6258
6346
  return false;
6259
6347
  }
6260
6348
  hasCaptionTracks() {
6349
+ var _a, _b;
6261
6350
  const textTracks = this.player.element.textTracks;
6262
6351
  for (let i = 0; i < textTracks.length; i++) {
6263
- if (textTracks[i].kind === "captions" || textTracks[i].kind === "subtitles") {
6264
- return true;
6265
- }
6352
+ if (textTracks[i].kind === "captions" || textTracks[i].kind === "subtitles") return true;
6353
+ }
6354
+ const trackEls = Array.from(this.player.element.querySelectorAll("track"));
6355
+ if (trackEls.some((el) => el.getAttribute("kind") === "captions" || el.getAttribute("kind") === "subtitles")) {
6356
+ return true;
6357
+ }
6358
+ const current = (_b = (_a = this.player.playlistManager) == null ? void 0 : _a.getCurrentTrack) == null ? void 0 : _b.call(_a);
6359
+ if ((current == null ? void 0 : current.tracks) && Array.isArray(current.tracks)) {
6360
+ return current.tracks.some((t) => (t == null ? void 0 : t.kind) === "captions" || (t == null ? void 0 : t.kind) === "subtitles");
6266
6361
  }
6267
6362
  return false;
6268
6363
  }
@@ -6337,55 +6432,65 @@
6337
6432
  this.currentPreviewTime = null;
6338
6433
  this.previewThumbnailTimeout = null;
6339
6434
  this.previewSupported = false;
6435
+ this.previewVideoReady = false;
6436
+ this.previewVideoInitialized = false;
6340
6437
  const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
6341
6438
  if (!isVideo) {
6342
6439
  return;
6343
6440
  }
6441
+ }
6442
+ /**
6443
+ * Lazily create the hidden preview video (only after playback started once)
6444
+ */
6445
+ ensurePreviewVideoInitialized() {
6446
+ var _a, _b;
6447
+ if (this.previewVideoInitialized) return;
6448
+ if (!((_b = (_a = this.player) == null ? void 0 : _a.state) == null ? void 0 : _b.hasStartedPlayback)) return;
6344
6449
  const renderer = this.player.renderer;
6345
6450
  const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
6346
6451
  const isHTML5Renderer = hasVideoMedia && renderer.media === this.player.element && !renderer.hls && typeof renderer.seek === "function";
6347
6452
  this.previewSupported = isHTML5Renderer && hasVideoMedia;
6348
- if (this.previewSupported) {
6349
- this.previewVideo = document.createElement("video");
6350
- this.previewVideo.muted = true;
6351
- this.previewVideo.preload = "auto";
6352
- this.previewVideo.playsInline = true;
6353
- this.previewVideo.style.position = "absolute";
6354
- this.previewVideo.style.visibility = "hidden";
6355
- this.previewVideo.style.width = "1px";
6356
- this.previewVideo.style.height = "1px";
6357
- this.previewVideo.style.top = "-9999px";
6358
- const mainVideo = renderer.media || this.player.element;
6359
- let videoSrc = null;
6360
- if (mainVideo.src) {
6361
- videoSrc = mainVideo.src;
6362
- } else {
6363
- const source = mainVideo.querySelector("source");
6364
- if (source) {
6365
- videoSrc = source.src;
6366
- }
6367
- }
6368
- if (!videoSrc) {
6369
- this.player.log("No video source found for preview", "warn");
6370
- this.previewSupported = false;
6371
- return;
6372
- }
6373
- if (mainVideo.crossOrigin) {
6374
- this.previewVideo.crossOrigin = mainVideo.crossOrigin;
6375
- }
6376
- this.previewVideo.addEventListener("error", (e) => {
6377
- this.player.log("Preview video failed to load:", e, "warn");
6378
- this.previewSupported = false;
6379
- });
6380
- this.previewVideo.addEventListener("loadedmetadata", () => {
6381
- this.previewVideoReady = true;
6382
- }, { once: true });
6383
- if (this.player.container) {
6384
- this.player.container.appendChild(this.previewVideo);
6453
+ if (!this.previewSupported) return;
6454
+ const mainVideo = renderer.media || this.player.element;
6455
+ let videoSrc = null;
6456
+ if (mainVideo.src) {
6457
+ videoSrc = mainVideo.src;
6458
+ } else {
6459
+ const source = mainVideo.querySelector("source");
6460
+ if (source) {
6461
+ videoSrc = source.src;
6385
6462
  }
6386
- this.previewVideo.src = videoSrc;
6387
- this.previewVideoReady = false;
6388
6463
  }
6464
+ if (!videoSrc) {
6465
+ this.player.log("No video source found for preview", "warn");
6466
+ this.previewSupported = false;
6467
+ return;
6468
+ }
6469
+ this.previewVideo = document.createElement("video");
6470
+ this.previewVideo.muted = true;
6471
+ this.previewVideo.preload = "auto";
6472
+ this.previewVideo.playsInline = true;
6473
+ this.previewVideo.style.position = "absolute";
6474
+ this.previewVideo.style.visibility = "hidden";
6475
+ this.previewVideo.style.width = "1px";
6476
+ this.previewVideo.style.height = "1px";
6477
+ this.previewVideo.style.top = "-9999px";
6478
+ if (mainVideo.crossOrigin) {
6479
+ this.previewVideo.crossOrigin = mainVideo.crossOrigin;
6480
+ }
6481
+ this.previewVideo.addEventListener("error", (e) => {
6482
+ this.player.log("Preview video failed to load:", e, "warn");
6483
+ this.previewSupported = false;
6484
+ });
6485
+ this.previewVideo.addEventListener("loadedmetadata", () => {
6486
+ this.previewVideoReady = true;
6487
+ }, { once: true });
6488
+ if (this.player.container) {
6489
+ this.player.container.appendChild(this.previewVideo);
6490
+ }
6491
+ this.previewVideo.src = videoSrc;
6492
+ this.previewVideoReady = false;
6493
+ this.previewVideoInitialized = true;
6389
6494
  }
6390
6495
  /**
6391
6496
  * Generate preview thumbnail for a specific time
@@ -6511,6 +6616,7 @@
6511
6616
  this.isDraggingProgress = false;
6512
6617
  });
6513
6618
  progress.addEventListener("mousemove", (e) => {
6619
+ var _a, _b;
6514
6620
  if (!this.isDraggingProgress) {
6515
6621
  const { time } = updateProgress(e.clientX);
6516
6622
  const rect = progress.getBoundingClientRect();
@@ -6518,8 +6624,17 @@
6518
6624
  this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
6519
6625
  this.controls.progressTooltip.style.left = "".concat(left, "px");
6520
6626
  this.controls.progressTooltip.style.display = "block";
6627
+ if (!((_b = (_a = this.player) == null ? void 0 : _a.state) == null ? void 0 : _b.hasStartedPlayback)) {
6628
+ if (this.controls.progressPreview) {
6629
+ this.controls.progressPreview.style.display = "none";
6630
+ }
6631
+ return;
6632
+ }
6633
+ this.ensurePreviewVideoInitialized();
6521
6634
  if (this.previewSupported) {
6522
6635
  this.updatePreviewThumbnail(time);
6636
+ } else if (this.controls.progressPreview) {
6637
+ this.controls.progressPreview.style.display = "none";
6523
6638
  }
6524
6639
  }
6525
6640
  });
@@ -10388,6 +10503,17 @@
10388
10503
  volume: 0.8,
10389
10504
  playbackSpeed: 1,
10390
10505
  preload: "metadata",
10506
+ // Optional initial duration (seconds) so UI can show duration
10507
+ // before media metadata is loaded (useful with deferLoad/preload=none).
10508
+ initialDuration: 0,
10509
+ // When enabled, VidPly will not start network loading during init().
10510
+ // - HTML5: does not call element.load() until the first user-initiated play()
10511
+ // - HLS (hls.js): does not load manifest/segments until the first play()
10512
+ // This is useful for pages with many players to avoid high initial bandwidth.
10513
+ deferLoad: false,
10514
+ // When enabled, clicking Audio Description / Sign Language before playback will show
10515
+ // a notice instead of implicitly starting playback/loading.
10516
+ requirePlaybackForAccessibilityToggles: false,
10391
10517
  startTime: 0,
10392
10518
  playsInline: true,
10393
10519
  // Enable inline playback on iOS (prevents native fullscreen)
@@ -10485,6 +10611,8 @@
10485
10611
  }, options);
10486
10612
  this.options.metadataAlerts = this.options.metadataAlerts || {};
10487
10613
  this.options.metadataHashtags = this.options.metadataHashtags || {};
10614
+ this.noticeElement = null;
10615
+ this.noticeTimeout = null;
10488
10616
  this.storage = new StorageManager("vidply");
10489
10617
  const savedPrefs = this.storage.getPlayerPreferences();
10490
10618
  if (savedPrefs) {
@@ -10499,10 +10627,11 @@
10499
10627
  ended: false,
10500
10628
  buffering: false,
10501
10629
  seeking: false,
10630
+ hasStartedPlayback: false,
10502
10631
  muted: this.options.muted,
10503
10632
  volume: this.options.volume,
10504
10633
  currentTime: 0,
10505
- duration: 0,
10634
+ duration: Number(this.options.initialDuration) > 0 ? Number(this.options.initialDuration) : 0,
10506
10635
  playbackSpeed: this.options.playbackSpeed,
10507
10636
  fullscreen: false,
10508
10637
  pip: false,
@@ -10591,6 +10720,52 @@
10591
10720
  });
10592
10721
  this.init();
10593
10722
  }
10723
+ /**
10724
+ * Show a small in-player notice (non-blocking), also announced to screen readers.
10725
+ */
10726
+ showNotice(message, { timeout = 2500, priority = "polite" } = {}) {
10727
+ var _a;
10728
+ try {
10729
+ if (!message) return;
10730
+ if (!this.container) return;
10731
+ if ((_a = this.keyboardManager) == null ? void 0 : _a.announce) {
10732
+ this.keyboardManager.announce(message, priority);
10733
+ }
10734
+ if (!this.noticeElement) {
10735
+ const el = document.createElement("div");
10736
+ el.className = "".concat(this.options.classPrefix, "-notice");
10737
+ el.setAttribute("role", "status");
10738
+ el.setAttribute("aria-live", priority);
10739
+ el.setAttribute("aria-atomic", "true");
10740
+ el.style.position = "absolute";
10741
+ el.style.left = "0.75rem";
10742
+ el.style.right = "0.75rem";
10743
+ el.style.top = "0.75rem";
10744
+ el.style.zIndex = "9999";
10745
+ el.style.padding = "0.5rem 0.75rem";
10746
+ el.style.borderRadius = "0.5rem";
10747
+ el.style.background = "rgba(0, 0, 0, 0.75)";
10748
+ el.style.color = "#fff";
10749
+ el.style.fontSize = "0.875rem";
10750
+ el.style.lineHeight = "1.3";
10751
+ el.style.pointerEvents = "none";
10752
+ this.noticeElement = el;
10753
+ this.container.appendChild(el);
10754
+ }
10755
+ this.noticeElement.textContent = message;
10756
+ this.noticeElement.style.display = "block";
10757
+ if (this.noticeTimeout) {
10758
+ clearTimeout(this.noticeTimeout);
10759
+ this.noticeTimeout = null;
10760
+ }
10761
+ this.noticeTimeout = setTimeout(() => {
10762
+ if (this.noticeElement) {
10763
+ this.noticeElement.style.display = "none";
10764
+ }
10765
+ }, timeout);
10766
+ } catch (e) {
10767
+ }
10768
+ }
10594
10769
  async init() {
10595
10770
  var _a;
10596
10771
  try {
@@ -10767,8 +10942,23 @@
10767
10942
  if (this.options.height) {
10768
10943
  this.container.style.height = typeof this.options.height === "number" ? "".concat(this.options.height, "px") : this.options.height;
10769
10944
  }
10945
+ if (this.element.tagName === "VIDEO" && !this.options.height) {
10946
+ const wAttr = parseInt(this.element.getAttribute("width") || "", 10);
10947
+ const hAttr = parseInt(this.element.getAttribute("height") || "", 10);
10948
+ if (Number.isFinite(wAttr) && Number.isFinite(hAttr) && wAttr > 0 && hAttr > 0) {
10949
+ if (!this.container.style.aspectRatio) {
10950
+ this.container.style.aspectRatio = "".concat(wAttr, " / ").concat(hAttr);
10951
+ }
10952
+ if (this.videoWrapper && !this.videoWrapper.style.aspectRatio) {
10953
+ this.videoWrapper.style.aspectRatio = "".concat(wAttr, " / ").concat(hAttr);
10954
+ this.videoWrapper.style.height = "auto";
10955
+ }
10956
+ }
10957
+ }
10770
10958
  if (this.options.poster && this.element.tagName === "VIDEO") {
10771
- this.element.poster = this.resolvePosterPath(this.options.poster);
10959
+ const resolvedPoster = this.resolvePosterPath(this.options.poster);
10960
+ this.element.poster = resolvedPoster;
10961
+ this.applyPosterAspectRatio(resolvedPoster);
10772
10962
  }
10773
10963
  if (this.element.tagName === "VIDEO") {
10774
10964
  this.createPlayButtonOverlay();
@@ -10782,6 +10972,7 @@
10782
10972
  }
10783
10973
  });
10784
10974
  this.on("play", () => {
10975
+ this.state.hasStartedPlayback = true;
10785
10976
  this.hidePosterOverlay();
10786
10977
  });
10787
10978
  this.on("timeupdate", () => {
@@ -10795,6 +10986,34 @@
10795
10986
  }
10796
10987
  }, { once: true });
10797
10988
  }
10989
+ /**
10990
+ * Apply aspect ratio to the video wrapper based on the poster's intrinsic size.
10991
+ * This helps render correct poster sizing before media metadata is available.
10992
+ */
10993
+ applyPosterAspectRatio(posterUrl) {
10994
+ try {
10995
+ if (!posterUrl) return;
10996
+ if (this.element.tagName !== "VIDEO") return;
10997
+ if (!this.videoWrapper) return;
10998
+ if (this.options.width || this.options.height) return;
10999
+ if (this._posterAspectAppliedFor === posterUrl) return;
11000
+ this._posterAspectAppliedFor = posterUrl;
11001
+ const img = new Image();
11002
+ img.decoding = "async";
11003
+ img.onload = () => {
11004
+ const w = img.naturalWidth;
11005
+ const h = img.naturalHeight;
11006
+ if (!w || !h) return;
11007
+ this.videoWrapper.style.aspectRatio = "".concat(w, " / ").concat(h);
11008
+ this.videoWrapper.style.height = "auto";
11009
+ if (this.container && !this.container.style.aspectRatio) {
11010
+ this.container.style.aspectRatio = "".concat(w, " / ").concat(h);
11011
+ }
11012
+ };
11013
+ img.src = posterUrl;
11014
+ } catch (e) {
11015
+ }
11016
+ }
10798
11017
  createPlayButtonOverlay() {
10799
11018
  this.playButtonOverlay = createPlayOverlay();
10800
11019
  this.playButtonOverlay.addEventListener("click", () => {
@@ -11193,7 +11412,25 @@
11193
11412
  await this.initializeRenderer();
11194
11413
  } else {
11195
11414
  this.renderer.media = this.element;
11196
- this.element.load();
11415
+ if (this.options.deferLoad) {
11416
+ try {
11417
+ this.element.preload = this.options.preload || "metadata";
11418
+ } catch (e) {
11419
+ }
11420
+ if (this.renderer) {
11421
+ if (typeof this.renderer._didDeferredLoad === "boolean") {
11422
+ this.renderer._didDeferredLoad = false;
11423
+ }
11424
+ if (typeof this.renderer._hlsSourceLoaded === "boolean") {
11425
+ this.renderer._hlsSourceLoaded = false;
11426
+ }
11427
+ if ("_pendingSrc" in this.renderer) {
11428
+ this.renderer._pendingSrc = this._pendingSource || this.currentSource || null;
11429
+ }
11430
+ }
11431
+ } else {
11432
+ this.element.load();
11433
+ }
11197
11434
  }
11198
11435
  if (isExternalRenderer) {
11199
11436
  setTimeout(() => {
@@ -11238,6 +11475,20 @@
11238
11475
  this.handleError(error);
11239
11476
  }
11240
11477
  }
11478
+ /**
11479
+ * Ensure the current renderer has started its initial load (metadata/manifest)
11480
+ * without starting playback. This is useful for playlists to behave like
11481
+ * single videos on selection, while still keeping autoplay off.
11482
+ */
11483
+ ensureLoaded() {
11484
+ try {
11485
+ if (!this.renderer) return;
11486
+ if (typeof this.renderer.ensureLoaded === "function") {
11487
+ this.renderer.ensureLoaded();
11488
+ }
11489
+ } catch (e) {
11490
+ }
11491
+ }
11241
11492
  /**
11242
11493
  * Check if we need to change renderer type
11243
11494
  * @param {string} src - New source URL
@@ -11273,6 +11524,11 @@
11273
11524
  play() {
11274
11525
  if (this.renderer) {
11275
11526
  this.renderer.play();
11527
+ return;
11528
+ }
11529
+ if (this.playlistManager && Array.isArray(this.playlistManager.tracks) && this.playlistManager.tracks.length > 0) {
11530
+ const index = this.playlistManager.currentIndex >= 0 ? this.playlistManager.currentIndex : 0;
11531
+ this.playlistManager.play(index, true);
11276
11532
  }
11277
11533
  }
11278
11534
  pause() {
@@ -12602,6 +12858,18 @@
12602
12858
  this.emit("audiodescriptiondisabled");
12603
12859
  }
12604
12860
  async toggleAudioDescription() {
12861
+ var _a, _b, _c;
12862
+ if (this.options.requirePlaybackForAccessibilityToggles && !this.renderer && ((_b = (_a = this.playlistManager) == null ? void 0 : _a.tracks) == null ? void 0 : _b.length)) {
12863
+ this.showNotice(i18n.t("player.startPlaybackForAudioDescription"));
12864
+ return;
12865
+ }
12866
+ if (!this.renderer && this.playlistManager && ((_c = this.playlistManager.tracks) == null ? void 0 : _c.length)) {
12867
+ this.audioDescriptionManager.desiredState = !this.audioDescriptionManager.desiredState;
12868
+ this.state.audioDescriptionEnabled = this.audioDescriptionManager.desiredState;
12869
+ this.emit(this.audioDescriptionManager.desiredState ? "audiodescriptionenabled" : "audiodescriptiondisabled");
12870
+ this.play();
12871
+ return;
12872
+ }
12605
12873
  return this.audioDescriptionManager.toggle();
12606
12874
  }
12607
12875
  // Sign Language (delegated to SignLanguageManager)
@@ -12865,6 +13133,19 @@
12865
13133
  return this.signLanguageManager.disable();
12866
13134
  }
12867
13135
  toggleSignLanguage() {
13136
+ var _a, _b, _c;
13137
+ if (this.options.requirePlaybackForAccessibilityToggles && !this.renderer && ((_b = (_a = this.playlistManager) == null ? void 0 : _a.tracks) == null ? void 0 : _b.length)) {
13138
+ this.showNotice(i18n.t("player.startPlaybackForSignLanguage"));
13139
+ return;
13140
+ }
13141
+ if (!this.renderer && this.playlistManager && ((_c = this.playlistManager.tracks) == null ? void 0 : _c.length)) {
13142
+ const wasEnabled = this.signLanguageManager.enabled;
13143
+ const result = this.signLanguageManager.toggle();
13144
+ if (!wasEnabled && this.signLanguageManager.enabled) {
13145
+ this.play();
13146
+ }
13147
+ return result;
13148
+ }
12868
13149
  return this.signLanguageManager.toggle();
12869
13150
  }
12870
13151
  setupSignLanguageInteraction() {
@@ -13976,6 +14257,7 @@
13976
14257
  * @param {boolean} autoPlay - Whether to auto-play after creation
13977
14258
  */
13978
14259
  async recreatePlayerForTrack(track, autoPlay = false) {
14260
+ var _a;
13979
14261
  if (!this.hostElement || !this.PlayerClass) {
13980
14262
  console.warn("VidPly Playlist: Cannot recreate player - missing hostElement or PlayerClass");
13981
14263
  return false;
@@ -13997,6 +14279,7 @@
13997
14279
  if (this.playlistPanel && this.playlistPanel.parentNode) {
13998
14280
  this.playlistPanel.parentNode.removeChild(this.playlistPanel);
13999
14281
  }
14282
+ const preservedPlayerOptions = ((_a = this.player) == null ? void 0 : _a.options) ? __spreadValues({}, this.player.options) : {};
14000
14283
  if (this.player) {
14001
14284
  this.player.off("ended", this.handleTrackEnd);
14002
14285
  this.player.off("error", this.handleTrackError);
@@ -14004,7 +14287,8 @@
14004
14287
  }
14005
14288
  this.hostElement.innerHTML = "";
14006
14289
  const mediaElement = document.createElement(elementType);
14007
- mediaElement.setAttribute("preload", "metadata");
14290
+ const preloadValue = preservedPlayerOptions.preload || "metadata";
14291
+ mediaElement.setAttribute("preload", preloadValue);
14008
14292
  if (elementType === "video" && track.poster && (mediaType === "video" || mediaType === "hls")) {
14009
14293
  mediaElement.setAttribute("poster", track.poster);
14010
14294
  }
@@ -14038,6 +14322,7 @@
14038
14322
  audioDescriptionDuration: track.audioDescriptionDuration || null,
14039
14323
  signLanguageSrc: track.signLanguageSrc || null
14040
14324
  };
14325
+ Object.assign(playerOptions, preservedPlayerOptions);
14041
14326
  this.player = new this.PlayerClass(mediaElement, playerOptions);
14042
14327
  this.player.playlistManager = this;
14043
14328
  await new Promise((resolve) => {
@@ -14200,31 +14485,35 @@
14200
14485
  if (this.options.autoPlayFirst) {
14201
14486
  this.play(0);
14202
14487
  } else {
14203
- this.loadTrack(0);
14488
+ void this.loadTrack(0).catch(() => {
14489
+ });
14204
14490
  }
14205
14491
  }
14206
14492
  this.updatePlaylistVisibilityInFullscreen();
14207
14493
  }
14208
14494
  /**
14209
14495
  * Load a track without playing
14496
+ * This is the playlist equivalent of a "single video initialized but not started yet":
14497
+ * it updates UI selection and loads the media into the player so metadata/manifests
14498
+ * and feature managers can be ready, but it does not start playback.
14210
14499
  * @param {number} index - Track index
14211
14500
  */
14212
14501
  async loadTrack(index) {
14502
+ var _a, _b;
14213
14503
  if (index < 0 || index >= this.tracks.length) {
14214
14504
  console.warn("VidPly Playlist: Invalid track index", index);
14215
14505
  return;
14216
14506
  }
14217
14507
  const track = this.tracks[index];
14508
+ this.selectTrack(index);
14218
14509
  this.isChangingTrack = true;
14219
- this.currentIndex = index;
14220
14510
  if (this.options.recreatePlayers && this.hostElement && this.PlayerClass) {
14221
14511
  const currentMediaType = this.player ? this.player.element.tagName === "AUDIO" ? "audio" : "video" : null;
14222
14512
  const newMediaType = this.getTrackMediaType(track);
14223
14513
  const newElementType = newMediaType === "audio" || newMediaType === "soundcloud" ? "audio" : "video";
14224
14514
  if (currentMediaType !== newElementType) {
14225
14515
  await this.recreatePlayerForTrack(track, false);
14226
- this.updateTrackInfo(track);
14227
- this.updatePlaylistUI();
14516
+ this.selectTrack(index);
14228
14517
  this.player.emit("playlisttrackchange", {
14229
14518
  index,
14230
14519
  item: track,
@@ -14236,16 +14525,22 @@
14236
14525
  return;
14237
14526
  }
14238
14527
  }
14239
- this.player.load({
14528
+ const loadPromise = this.player.load({
14240
14529
  src: track.src,
14241
14530
  type: track.type,
14242
14531
  poster: track.poster,
14243
14532
  tracks: track.tracks || [],
14244
14533
  audioDescriptionSrc: track.audioDescriptionSrc || null,
14245
- signLanguageSrc: track.signLanguageSrc || null
14246
- });
14247
- this.updateTrackInfo(track);
14248
- this.updatePlaylistUI();
14534
+ signLanguageSrc: track.signLanguageSrc || null,
14535
+ signLanguageSources: track.signLanguageSources || {}
14536
+ });
14537
+ if (((_b = (_a = this.player) == null ? void 0 : _a.options) == null ? void 0 : _b.deferLoad) && typeof this.player.ensureLoaded === "function") {
14538
+ Promise.resolve(loadPromise).then(() => {
14539
+ var _a2, _b2;
14540
+ return (_b2 = (_a2 = this.player) == null ? void 0 : _a2.ensureLoaded) == null ? void 0 : _b2.call(_a2);
14541
+ }).catch(() => {
14542
+ });
14543
+ }
14249
14544
  this.player.emit("playlisttrackchange", {
14250
14545
  index,
14251
14546
  item: track,
@@ -14255,12 +14550,105 @@
14255
14550
  this.isChangingTrack = false;
14256
14551
  }, 150);
14257
14552
  }
14553
+ /**
14554
+ * Select a track (UI/selection only; does NOT set the media src / does NOT initialize renderer)
14555
+ *
14556
+ * In "B always" playlist mode, you typically want `loadTrack()` on selection so the
14557
+ * selected item behaves like a single video (metadata/manifest loaded, features ready)
14558
+ * without auto-playing.
14559
+ * @param {number} index - Track index
14560
+ */
14561
+ selectTrack(index) {
14562
+ var _a, _b, _c, _d;
14563
+ if (index < 0 || index >= this.tracks.length) {
14564
+ console.warn("VidPly Playlist: Invalid track index", index);
14565
+ return;
14566
+ }
14567
+ const track = this.tracks[index];
14568
+ this.currentIndex = index;
14569
+ try {
14570
+ if (((_b = (_a = this.player) == null ? void 0 : _a.element) == null ? void 0 : _b.tagName) === "VIDEO") {
14571
+ if (track.poster) {
14572
+ const posterUrl = typeof this.player.resolvePosterPath === "function" ? this.player.resolvePosterPath(track.poster) : track.poster;
14573
+ this.player.element.poster = posterUrl;
14574
+ (_d = (_c = this.player).applyPosterAspectRatio) == null ? void 0 : _d.call(_c, posterUrl);
14575
+ } else {
14576
+ this.player.element.removeAttribute("poster");
14577
+ }
14578
+ }
14579
+ this.player.audioDescriptionSrc = track.audioDescriptionSrc || null;
14580
+ this.player.signLanguageSrc = track.signLanguageSrc || null;
14581
+ this.player.signLanguageSources = track.signLanguageSources || {};
14582
+ if (track.duration && Number(track.duration) > 0) {
14583
+ this.player.state.duration = Number(track.duration);
14584
+ }
14585
+ if (this.player.audioDescriptionManager) {
14586
+ this.player.audioDescriptionManager.src = track.audioDescriptionSrc || null;
14587
+ this.player.audioDescriptionManager.originalSource = track.src || this.player.originalSrc || null;
14588
+ }
14589
+ if (this.player.signLanguageManager) {
14590
+ this.player.signLanguageManager.src = track.signLanguageSrc || null;
14591
+ this.player.signLanguageManager.sources = track.signLanguageSources || {};
14592
+ this.player.signLanguageManager.currentLanguage = null;
14593
+ }
14594
+ if (track.src && !this.player.originalSrc) {
14595
+ this.player.originalSrc = track.src;
14596
+ }
14597
+ const existing = Array.from(this.player.element.querySelectorAll("track"));
14598
+ existing.forEach((t) => t.remove());
14599
+ if (Array.isArray(track.tracks)) {
14600
+ track.tracks.forEach((tc) => {
14601
+ if (!(tc == null ? void 0 : tc.src)) return;
14602
+ const el = document.createElement("track");
14603
+ el.src = tc.src;
14604
+ el.kind = tc.kind || "captions";
14605
+ el.srclang = tc.srclang || "en";
14606
+ el.label = tc.label || tc.srclang || "Track";
14607
+ if (tc.default) el.default = true;
14608
+ if (tc.describedSrc) {
14609
+ el.setAttribute("data-desc-src", tc.describedSrc);
14610
+ }
14611
+ this.player.element.appendChild(el);
14612
+ });
14613
+ }
14614
+ if (typeof this.player.invalidateTrackCache === "function") {
14615
+ this.player.invalidateTrackCache();
14616
+ }
14617
+ if (this.player.audioDescriptionManager && typeof this.player.audioDescriptionManager.initFromSourceElements === "function") {
14618
+ try {
14619
+ this.player.audioDescriptionManager.captionTracks = [];
14620
+ this.player.audioDescriptionManager.initFromSourceElements(this.player.sourceElements, this.player.trackElements);
14621
+ } catch (e) {
14622
+ }
14623
+ }
14624
+ if (this.player.captionManager && typeof this.player.captionManager.loadTracks === "function") {
14625
+ try {
14626
+ this.player.captionManager.tracks = [];
14627
+ this.player.captionManager.currentTrack = null;
14628
+ this.player.captionManager.loadTracks();
14629
+ } catch (e) {
14630
+ }
14631
+ }
14632
+ if (typeof this.player.updateControlBar === "function") {
14633
+ this.player.updateControlBar();
14634
+ }
14635
+ } catch (e) {
14636
+ }
14637
+ this.updateTrackInfo(track);
14638
+ this.updatePlaylistUI();
14639
+ this.player.emit("playlisttrackselect", {
14640
+ index,
14641
+ item: track,
14642
+ total: this.tracks.length
14643
+ });
14644
+ }
14258
14645
  /**
14259
14646
  * Play a specific track
14260
14647
  * @param {number} index - Track index
14261
14648
  * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
14262
14649
  */
14263
14650
  async play(index, userInitiated = false) {
14651
+ var _a, _b;
14264
14652
  if (index < 0 || index >= this.tracks.length) {
14265
14653
  console.warn("VidPly Playlist: Invalid track index", index);
14266
14654
  return;
@@ -14287,13 +14675,21 @@
14287
14675
  return;
14288
14676
  }
14289
14677
  }
14678
+ let srcToLoad = track.src;
14679
+ if (((_b = (_a = this.player) == null ? void 0 : _a.audioDescriptionManager) == null ? void 0 : _b.desiredState) && track.audioDescriptionSrc) {
14680
+ this.player.originalSrc = track.src;
14681
+ this.player.audioDescriptionManager.originalSource = track.src;
14682
+ this.player.audioDescriptionManager.src = track.audioDescriptionSrc;
14683
+ srcToLoad = track.audioDescriptionSrc;
14684
+ }
14290
14685
  this.player.load({
14291
- src: track.src,
14686
+ src: srcToLoad,
14292
14687
  type: track.type,
14293
14688
  poster: track.poster,
14294
14689
  tracks: track.tracks || [],
14295
14690
  audioDescriptionSrc: track.audioDescriptionSrc || null,
14296
- signLanguageSrc: track.signLanguageSrc || null
14691
+ signLanguageSrc: track.signLanguageSrc || null,
14692
+ signLanguageSources: track.signLanguageSources || {}
14297
14693
  });
14298
14694
  this.updateTrackInfo(track);
14299
14695
  this.updatePlaylistUI();
@@ -14474,21 +14870,6 @@
14474
14870
  console.warn("VidPly Playlist: No container found");
14475
14871
  return;
14476
14872
  }
14477
- if (this.player.element.tagName === "AUDIO") {
14478
- this.trackArtworkElement = DOMUtils.createElement("div", {
14479
- className: "vidply-track-artwork",
14480
- attributes: {
14481
- "aria-hidden": "true"
14482
- }
14483
- });
14484
- this.trackArtworkElement.style.display = "none";
14485
- const videoWrapper = this.container.querySelector(".vidply-video-wrapper");
14486
- if (videoWrapper) {
14487
- this.container.insertBefore(this.trackArtworkElement, videoWrapper);
14488
- } else {
14489
- this.container.appendChild(this.trackArtworkElement);
14490
- }
14491
- }
14492
14873
  this.trackInfoElement = DOMUtils.createElement("div", {
14493
14874
  className: "vidply-track-info",
14494
14875
  attributes: {
@@ -14552,6 +14933,28 @@
14552
14933
  * Update track artwork display (for audio playlists)
14553
14934
  */
14554
14935
  updateTrackArtwork(track) {
14936
+ var _a, _b;
14937
+ if (((_b = (_a = this.player) == null ? void 0 : _a.element) == null ? void 0 : _b.tagName) !== "AUDIO") {
14938
+ if (this.trackArtworkElement) {
14939
+ this.trackArtworkElement.style.display = "none";
14940
+ }
14941
+ return;
14942
+ }
14943
+ if (!this.trackArtworkElement && this.container) {
14944
+ this.trackArtworkElement = DOMUtils.createElement("div", {
14945
+ className: "vidply-track-artwork",
14946
+ attributes: {
14947
+ "aria-hidden": "true"
14948
+ }
14949
+ });
14950
+ this.trackArtworkElement.style.display = "none";
14951
+ const videoWrapper = this.container.querySelector(".vidply-video-wrapper");
14952
+ if (videoWrapper) {
14953
+ this.container.insertBefore(this.trackArtworkElement, videoWrapper);
14954
+ } else {
14955
+ this.container.appendChild(this.trackArtworkElement);
14956
+ }
14957
+ }
14555
14958
  if (!this.trackArtworkElement) return;
14556
14959
  if (track.poster) {
14557
14960
  this.trackArtworkElement.style.backgroundImage = "url(".concat(track.poster, ")");