vidply 1.0.31 → 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.
Files changed (52) hide show
  1. package/README.md +708 -708
  2. package/dist/dev/{vidply.HLSRenderer-ENLZE4QS.js → vidply.HLSRenderer-LIFBU6UD.js} +61 -10
  3. package/dist/dev/vidply.HLSRenderer-LIFBU6UD.js.map +7 -0
  4. package/dist/dev/{vidply.HTML5Renderer-6SBDI6S2.js → vidply.HTML5Renderer-YWMVYWFS.js} +2 -2
  5. package/dist/dev/{vidply.TranscriptManager-T677KF4N.js → vidply.TranscriptManager-R7NJRU7E.js} +2 -2
  6. package/dist/dev/{vidply.chunk-GS2JX5RQ.js → vidply.chunk-PMRKJBGH.js} +5 -2
  7. package/dist/dev/vidply.chunk-PMRKJBGH.js.map +7 -0
  8. package/dist/dev/{vidply.chunk-W2LSBD6Y.js → vidply.chunk-UVO24MXU.js} +33 -3
  9. package/dist/dev/vidply.chunk-UVO24MXU.js.map +7 -0
  10. package/dist/dev/{vidply.de-SNL6AJ4D.js → vidply.de-CEGBLV67.js} +4 -1
  11. package/dist/dev/vidply.de-CEGBLV67.js.map +7 -0
  12. package/dist/dev/vidply.esm.js +374 -64
  13. package/dist/dev/vidply.esm.js.map +2 -2
  14. package/dist/legacy/vidply.js +483 -71
  15. package/dist/legacy/vidply.js.map +3 -3
  16. package/dist/legacy/vidply.min.js +1 -1
  17. package/dist/legacy/vidply.min.meta.json +15 -15
  18. package/dist/prod/vidply.HLSRenderer-ESR6NAMI.min.js +6 -0
  19. package/dist/prod/{vidply.HTML5Renderer-KKW3OLHM.min.js → vidply.HTML5Renderer-6ROXQSQY.min.js} +1 -1
  20. package/dist/prod/{vidply.TranscriptManager-WFZSW6NR.min.js → vidply.TranscriptManager-B65LKXGG.min.js} +1 -1
  21. package/dist/prod/vidply.chunk-7HVHEUHH.min.js +6 -0
  22. package/dist/prod/vidply.chunk-IQKD4GUB.min.js +6 -0
  23. package/dist/prod/vidply.de-IHKC573T.min.js +6 -0
  24. package/dist/prod/vidply.esm.min.js +9 -9
  25. package/dist/vidply.esm.min.meta.json +33 -33
  26. package/package.json +1 -1
  27. package/src/controls/ControlBar.js +120 -71
  28. package/src/core/Player.js +5087 -4868
  29. package/src/features/PlaylistManager.js +1669 -1511
  30. package/src/i18n/languages/de.js +3 -0
  31. package/src/i18n/languages/en.js +3 -0
  32. package/src/renderers/HLSRenderer.js +77 -8
  33. package/src/renderers/HTML5Renderer.js +43 -5
  34. package/dist/dev/vidply.HLSRenderer-ENLZE4QS.js.map +0 -7
  35. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js +0 -266
  36. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js.map +0 -7
  37. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js +0 -12
  38. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js.map +0 -7
  39. package/dist/dev/vidply.chunk-BCOFCT6U.js +0 -246
  40. package/dist/dev/vidply.chunk-BCOFCT6U.js.map +0 -7
  41. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +0 -7
  42. package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +0 -7
  43. package/dist/dev/vidply.de-SNL6AJ4D.js.map +0 -7
  44. package/dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js +0 -6
  45. package/dist/prod/vidply.HLSRenderer-CBXZ4RF2.min.js +0 -6
  46. package/dist/prod/vidply.HTML5Renderer-MY7XDV7R.min.js +0 -6
  47. package/dist/prod/vidply.chunk-34RH2THY.min.js +0 -6
  48. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +0 -6
  49. package/dist/prod/vidply.chunk-OXXPY2XB.min.js +0 -6
  50. package/dist/prod/vidply.de-FR3XX54P.min.js +0 -6
  51. /package/dist/dev/{vidply.HTML5Renderer-6SBDI6S2.js.map → vidply.HTML5Renderer-YWMVYWFS.js.map} +0 -0
  52. /package/dist/dev/{vidply.TranscriptManager-T677KF4N.js.map → vidply.TranscriptManager-R7NJRU7E.js.map} +0 -0
@@ -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) {
@@ -5194,13 +5275,19 @@
5194
5275
  }
5195
5276
  getQualities() {
5196
5277
  if (this.hls && this.hls.levels) {
5197
- return this.hls.levels.map((level, index) => ({
5198
- index,
5199
- height: level.height,
5200
- width: level.width,
5201
- bitrate: level.bitrate,
5202
- name: "".concat(level.height, "p")
5203
- }));
5278
+ return this.hls.levels.map((level, index) => {
5279
+ const height = Number(level.height) || 0;
5280
+ const bitrate = Number(level.bitrate) || 0;
5281
+ const kb = bitrate > 0 ? Math.round(bitrate / 1e3) : 0;
5282
+ const name = height > 0 ? "".concat(height, "p") : kb > 0 ? "".concat(kb, " kb") : "Auto";
5283
+ return {
5284
+ index,
5285
+ height: level.height,
5286
+ width: level.width,
5287
+ bitrate: level.bitrate,
5288
+ name
5289
+ };
5290
+ });
5204
5291
  }
5205
5292
  return [];
5206
5293
  }
@@ -5677,6 +5764,8 @@
5677
5764
  init() {
5678
5765
  this.createElement();
5679
5766
  this.createControls();
5767
+ this.updateDuration();
5768
+ this.updateProgress();
5680
5769
  this.attachEvents();
5681
5770
  this.setupAutoHide();
5682
5771
  this.setupOverflowDetection();
@@ -6091,6 +6180,7 @@
6091
6180
  });
6092
6181
  }
6093
6182
  createControls() {
6183
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
6094
6184
  const progressTimeWrapper = DOMUtils.createElement("div", {
6095
6185
  className: "".concat(this.player.options.classPrefix, "-progress-time-wrapper")
6096
6186
  });
@@ -6156,7 +6246,11 @@
6156
6246
  btn.dataset.overflowPriorityMobile = "3";
6157
6247
  this.rightButtons.appendChild(btn);
6158
6248
  }
6159
- if (this.player.options.speedButton) {
6249
+ const src = this.player.currentSource || ((_b = (_a = this.player.element) == null ? void 0 : _a.getAttribute) == null ? void 0 : _b.call(_a, "src")) || ((_c = this.player.element) == null ? void 0 : _c.currentSrc) || ((_d = this.player.element) == null ? void 0 : _d.src) || ((_h = (_g = (_f = (_e = this.player.element) == null ? void 0 : _e.querySelector) == null ? void 0 : _f.call(_e, "source")) == null ? void 0 : _g.getAttribute) == null ? void 0 : _h.call(_g, "src")) || ((_k = (_j = (_i = this.player.element) == null ? void 0 : _i.querySelector) == null ? void 0 : _j.call(_i, "source")) == null ? void 0 : _k.src) || "";
6250
+ const isHlsSource = typeof src === "string" && src.includes(".m3u8");
6251
+ const isVideoElement = ((_m = (_l = this.player.element) == null ? void 0 : _l.tagName) == null ? void 0 : _m.toLowerCase()) === "video";
6252
+ const hideSpeedForThisPlayer = !!this.player.options.hideSpeedForHls && isHlsSource || !!this.player.options.hideSpeedForHlsVideo && isHlsSource && isVideoElement;
6253
+ if (this.player.options.speedButton && !hideSpeedForThisPlayer) {
6160
6254
  const btn = this.createSpeedButton();
6161
6255
  btn.dataset.overflowPriority = "1";
6162
6256
  btn.dataset.overflowPriorityMobile = "3";
@@ -6238,20 +6332,32 @@
6238
6332
  }
6239
6333
  // Helper methods to check for available features
6240
6334
  hasChapterTracks() {
6335
+ var _a, _b;
6241
6336
  const textTracks = this.player.element.textTracks;
6242
6337
  for (let i = 0; i < textTracks.length; i++) {
6243
- if (textTracks[i].kind === "chapters") {
6244
- return true;
6245
- }
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");
6246
6345
  }
6247
6346
  return false;
6248
6347
  }
6249
6348
  hasCaptionTracks() {
6349
+ var _a, _b;
6250
6350
  const textTracks = this.player.element.textTracks;
6251
6351
  for (let i = 0; i < textTracks.length; i++) {
6252
- if (textTracks[i].kind === "captions" || textTracks[i].kind === "subtitles") {
6253
- return true;
6254
- }
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");
6255
6361
  }
6256
6362
  return false;
6257
6363
  }
@@ -6326,55 +6432,65 @@
6326
6432
  this.currentPreviewTime = null;
6327
6433
  this.previewThumbnailTimeout = null;
6328
6434
  this.previewSupported = false;
6435
+ this.previewVideoReady = false;
6436
+ this.previewVideoInitialized = false;
6329
6437
  const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
6330
6438
  if (!isVideo) {
6331
6439
  return;
6332
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;
6333
6449
  const renderer = this.player.renderer;
6334
6450
  const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
6335
6451
  const isHTML5Renderer = hasVideoMedia && renderer.media === this.player.element && !renderer.hls && typeof renderer.seek === "function";
6336
6452
  this.previewSupported = isHTML5Renderer && hasVideoMedia;
6337
- if (this.previewSupported) {
6338
- this.previewVideo = document.createElement("video");
6339
- this.previewVideo.muted = true;
6340
- this.previewVideo.preload = "auto";
6341
- this.previewVideo.playsInline = true;
6342
- this.previewVideo.style.position = "absolute";
6343
- this.previewVideo.style.visibility = "hidden";
6344
- this.previewVideo.style.width = "1px";
6345
- this.previewVideo.style.height = "1px";
6346
- this.previewVideo.style.top = "-9999px";
6347
- const mainVideo = renderer.media || this.player.element;
6348
- let videoSrc = null;
6349
- if (mainVideo.src) {
6350
- videoSrc = mainVideo.src;
6351
- } else {
6352
- const source = mainVideo.querySelector("source");
6353
- if (source) {
6354
- videoSrc = source.src;
6355
- }
6356
- }
6357
- if (!videoSrc) {
6358
- this.player.log("No video source found for preview", "warn");
6359
- this.previewSupported = false;
6360
- return;
6361
- }
6362
- if (mainVideo.crossOrigin) {
6363
- this.previewVideo.crossOrigin = mainVideo.crossOrigin;
6364
- }
6365
- this.previewVideo.addEventListener("error", (e) => {
6366
- this.player.log("Preview video failed to load:", e, "warn");
6367
- this.previewSupported = false;
6368
- });
6369
- this.previewVideo.addEventListener("loadedmetadata", () => {
6370
- this.previewVideoReady = true;
6371
- }, { once: true });
6372
- if (this.player.container) {
6373
- 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;
6374
6462
  }
6375
- this.previewVideo.src = videoSrc;
6376
- this.previewVideoReady = false;
6377
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;
6378
6494
  }
6379
6495
  /**
6380
6496
  * Generate preview thumbnail for a specific time
@@ -6500,6 +6616,7 @@
6500
6616
  this.isDraggingProgress = false;
6501
6617
  });
6502
6618
  progress.addEventListener("mousemove", (e) => {
6619
+ var _a, _b;
6503
6620
  if (!this.isDraggingProgress) {
6504
6621
  const { time } = updateProgress(e.clientX);
6505
6622
  const rect = progress.getBoundingClientRect();
@@ -6507,8 +6624,17 @@
6507
6624
  this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
6508
6625
  this.controls.progressTooltip.style.left = "".concat(left, "px");
6509
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();
6510
6634
  if (this.previewSupported) {
6511
6635
  this.updatePreviewThumbnail(time);
6636
+ } else if (this.controls.progressPreview) {
6637
+ this.controls.progressPreview.style.display = "none";
6512
6638
  }
6513
6639
  }
6514
6640
  });
@@ -10377,6 +10503,17 @@
10377
10503
  volume: 0.8,
10378
10504
  playbackSpeed: 1,
10379
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,
10380
10517
  startTime: 0,
10381
10518
  playsInline: true,
10382
10519
  // Enable inline playback on iOS (prevents native fullscreen)
@@ -10393,6 +10530,11 @@
10393
10530
  qualityButton: true,
10394
10531
  captionStyleButton: true,
10395
10532
  speedButton: true,
10533
+ // When enabled, the playback speed UI is suppressed for ALL HLS streams (audio + video).
10534
+ hideSpeedForHls: false,
10535
+ // When enabled, the playback speed UI is suppressed for HLS *video* streams only.
10536
+ // This is useful for live streams where speed controls don't make sense.
10537
+ hideSpeedForHlsVideo: false,
10396
10538
  captionsButton: true,
10397
10539
  transcriptButton: true,
10398
10540
  fullscreenButton: true,
@@ -10469,6 +10611,8 @@
10469
10611
  }, options);
10470
10612
  this.options.metadataAlerts = this.options.metadataAlerts || {};
10471
10613
  this.options.metadataHashtags = this.options.metadataHashtags || {};
10614
+ this.noticeElement = null;
10615
+ this.noticeTimeout = null;
10472
10616
  this.storage = new StorageManager("vidply");
10473
10617
  const savedPrefs = this.storage.getPlayerPreferences();
10474
10618
  if (savedPrefs) {
@@ -10483,10 +10627,11 @@
10483
10627
  ended: false,
10484
10628
  buffering: false,
10485
10629
  seeking: false,
10630
+ hasStartedPlayback: false,
10486
10631
  muted: this.options.muted,
10487
10632
  volume: this.options.volume,
10488
10633
  currentTime: 0,
10489
- duration: 0,
10634
+ duration: Number(this.options.initialDuration) > 0 ? Number(this.options.initialDuration) : 0,
10490
10635
  playbackSpeed: this.options.playbackSpeed,
10491
10636
  fullscreen: false,
10492
10637
  pip: false,
@@ -10575,6 +10720,52 @@
10575
10720
  });
10576
10721
  this.init();
10577
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
+ }
10578
10769
  async init() {
10579
10770
  var _a;
10580
10771
  try {
@@ -10751,8 +10942,23 @@
10751
10942
  if (this.options.height) {
10752
10943
  this.container.style.height = typeof this.options.height === "number" ? "".concat(this.options.height, "px") : this.options.height;
10753
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
+ }
10754
10958
  if (this.options.poster && this.element.tagName === "VIDEO") {
10755
- 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);
10756
10962
  }
10757
10963
  if (this.element.tagName === "VIDEO") {
10758
10964
  this.createPlayButtonOverlay();
@@ -10766,6 +10972,7 @@
10766
10972
  }
10767
10973
  });
10768
10974
  this.on("play", () => {
10975
+ this.state.hasStartedPlayback = true;
10769
10976
  this.hidePosterOverlay();
10770
10977
  });
10771
10978
  this.on("timeupdate", () => {
@@ -10779,6 +10986,34 @@
10779
10986
  }
10780
10987
  }, { once: true });
10781
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
+ }
10782
11017
  createPlayButtonOverlay() {
10783
11018
  this.playButtonOverlay = createPlayOverlay();
10784
11019
  this.playButtonOverlay.addEventListener("click", () => {
@@ -11177,7 +11412,25 @@
11177
11412
  await this.initializeRenderer();
11178
11413
  } else {
11179
11414
  this.renderer.media = this.element;
11180
- 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
+ }
11181
11434
  }
11182
11435
  if (isExternalRenderer) {
11183
11436
  setTimeout(() => {
@@ -11222,6 +11475,20 @@
11222
11475
  this.handleError(error);
11223
11476
  }
11224
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
+ }
11225
11492
  /**
11226
11493
  * Check if we need to change renderer type
11227
11494
  * @param {string} src - New source URL
@@ -11257,6 +11524,11 @@
11257
11524
  play() {
11258
11525
  if (this.renderer) {
11259
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);
11260
11532
  }
11261
11533
  }
11262
11534
  pause() {
@@ -12586,6 +12858,18 @@
12586
12858
  this.emit("audiodescriptiondisabled");
12587
12859
  }
12588
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
+ }
12589
12873
  return this.audioDescriptionManager.toggle();
12590
12874
  }
12591
12875
  // Sign Language (delegated to SignLanguageManager)
@@ -12849,6 +13133,19 @@
12849
13133
  return this.signLanguageManager.disable();
12850
13134
  }
12851
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
+ }
12852
13149
  return this.signLanguageManager.toggle();
12853
13150
  }
12854
13151
  setupSignLanguageInteraction() {
@@ -13960,6 +14257,7 @@
13960
14257
  * @param {boolean} autoPlay - Whether to auto-play after creation
13961
14258
  */
13962
14259
  async recreatePlayerForTrack(track, autoPlay = false) {
14260
+ var _a;
13963
14261
  if (!this.hostElement || !this.PlayerClass) {
13964
14262
  console.warn("VidPly Playlist: Cannot recreate player - missing hostElement or PlayerClass");
13965
14263
  return false;
@@ -13981,6 +14279,7 @@
13981
14279
  if (this.playlistPanel && this.playlistPanel.parentNode) {
13982
14280
  this.playlistPanel.parentNode.removeChild(this.playlistPanel);
13983
14281
  }
14282
+ const preservedPlayerOptions = ((_a = this.player) == null ? void 0 : _a.options) ? __spreadValues({}, this.player.options) : {};
13984
14283
  if (this.player) {
13985
14284
  this.player.off("ended", this.handleTrackEnd);
13986
14285
  this.player.off("error", this.handleTrackError);
@@ -13988,7 +14287,8 @@
13988
14287
  }
13989
14288
  this.hostElement.innerHTML = "";
13990
14289
  const mediaElement = document.createElement(elementType);
13991
- mediaElement.setAttribute("preload", "metadata");
14290
+ const preloadValue = preservedPlayerOptions.preload || "metadata";
14291
+ mediaElement.setAttribute("preload", preloadValue);
13992
14292
  if (elementType === "video" && track.poster && (mediaType === "video" || mediaType === "hls")) {
13993
14293
  mediaElement.setAttribute("poster", track.poster);
13994
14294
  }
@@ -14022,6 +14322,7 @@
14022
14322
  audioDescriptionDuration: track.audioDescriptionDuration || null,
14023
14323
  signLanguageSrc: track.signLanguageSrc || null
14024
14324
  };
14325
+ Object.assign(playerOptions, preservedPlayerOptions);
14025
14326
  this.player = new this.PlayerClass(mediaElement, playerOptions);
14026
14327
  this.player.playlistManager = this;
14027
14328
  await new Promise((resolve) => {
@@ -14184,31 +14485,35 @@
14184
14485
  if (this.options.autoPlayFirst) {
14185
14486
  this.play(0);
14186
14487
  } else {
14187
- this.loadTrack(0);
14488
+ void this.loadTrack(0).catch(() => {
14489
+ });
14188
14490
  }
14189
14491
  }
14190
14492
  this.updatePlaylistVisibilityInFullscreen();
14191
14493
  }
14192
14494
  /**
14193
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.
14194
14499
  * @param {number} index - Track index
14195
14500
  */
14196
14501
  async loadTrack(index) {
14502
+ var _a, _b;
14197
14503
  if (index < 0 || index >= this.tracks.length) {
14198
14504
  console.warn("VidPly Playlist: Invalid track index", index);
14199
14505
  return;
14200
14506
  }
14201
14507
  const track = this.tracks[index];
14508
+ this.selectTrack(index);
14202
14509
  this.isChangingTrack = true;
14203
- this.currentIndex = index;
14204
14510
  if (this.options.recreatePlayers && this.hostElement && this.PlayerClass) {
14205
14511
  const currentMediaType = this.player ? this.player.element.tagName === "AUDIO" ? "audio" : "video" : null;
14206
14512
  const newMediaType = this.getTrackMediaType(track);
14207
14513
  const newElementType = newMediaType === "audio" || newMediaType === "soundcloud" ? "audio" : "video";
14208
14514
  if (currentMediaType !== newElementType) {
14209
14515
  await this.recreatePlayerForTrack(track, false);
14210
- this.updateTrackInfo(track);
14211
- this.updatePlaylistUI();
14516
+ this.selectTrack(index);
14212
14517
  this.player.emit("playlisttrackchange", {
14213
14518
  index,
14214
14519
  item: track,
@@ -14220,16 +14525,22 @@
14220
14525
  return;
14221
14526
  }
14222
14527
  }
14223
- this.player.load({
14528
+ const loadPromise = this.player.load({
14224
14529
  src: track.src,
14225
14530
  type: track.type,
14226
14531
  poster: track.poster,
14227
14532
  tracks: track.tracks || [],
14228
14533
  audioDescriptionSrc: track.audioDescriptionSrc || null,
14229
- signLanguageSrc: track.signLanguageSrc || null
14230
- });
14231
- this.updateTrackInfo(track);
14232
- 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
+ }
14233
14544
  this.player.emit("playlisttrackchange", {
14234
14545
  index,
14235
14546
  item: track,
@@ -14239,12 +14550,105 @@
14239
14550
  this.isChangingTrack = false;
14240
14551
  }, 150);
14241
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
+ }
14242
14645
  /**
14243
14646
  * Play a specific track
14244
14647
  * @param {number} index - Track index
14245
14648
  * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
14246
14649
  */
14247
14650
  async play(index, userInitiated = false) {
14651
+ var _a, _b;
14248
14652
  if (index < 0 || index >= this.tracks.length) {
14249
14653
  console.warn("VidPly Playlist: Invalid track index", index);
14250
14654
  return;
@@ -14271,13 +14675,21 @@
14271
14675
  return;
14272
14676
  }
14273
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
+ }
14274
14685
  this.player.load({
14275
- src: track.src,
14686
+ src: srcToLoad,
14276
14687
  type: track.type,
14277
14688
  poster: track.poster,
14278
14689
  tracks: track.tracks || [],
14279
14690
  audioDescriptionSrc: track.audioDescriptionSrc || null,
14280
- signLanguageSrc: track.signLanguageSrc || null
14691
+ signLanguageSrc: track.signLanguageSrc || null,
14692
+ signLanguageSources: track.signLanguageSources || {}
14281
14693
  });
14282
14694
  this.updateTrackInfo(track);
14283
14695
  this.updatePlaylistUI();