vidply 1.0.7 → 1.0.9

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/dist/vidply.js CHANGED
@@ -520,7 +520,8 @@ var VidPly = (() => {
520
520
  resizeWindow: "Resize Window",
521
521
  styleTranscript: "Open transcript style settings",
522
522
  closeMenu: "Close Menu",
523
- styleTitle: "Transcript Style"
523
+ styleTitle: "Transcript Style",
524
+ autoscroll: "Autoscroll"
524
525
  },
525
526
  settings: {
526
527
  title: "Settings",
@@ -641,7 +642,8 @@ var VidPly = (() => {
641
642
  resizeWindow: "Fenster vergr\xF6\xDFern/verkleinern",
642
643
  styleTranscript: "Transkript-Stileinstellungen \xF6ffnen",
643
644
  closeMenu: "Men\xFC schlie\xDFen",
644
- styleTitle: "Transkript-Stil"
645
+ styleTitle: "Transkript-Stil",
646
+ autoscroll: "Automatisches Scrollen"
645
647
  },
646
648
  settings: {
647
649
  title: "Einstellungen",
@@ -762,7 +764,8 @@ var VidPly = (() => {
762
764
  resizeWindow: "Cambiar tama\xF1o de ventana",
763
765
  styleTranscript: "Abrir configuraci\xF3n de estilo de transcripci\xF3n",
764
766
  closeMenu: "Cerrar men\xFA",
765
- styleTitle: "Estilo de Transcripci\xF3n"
767
+ styleTitle: "Estilo de Transcripci\xF3n",
768
+ autoscroll: "Desplazamiento autom\xE1tico"
766
769
  },
767
770
  settings: {
768
771
  title: "Configuraci\xF3n",
@@ -883,7 +886,8 @@ var VidPly = (() => {
883
886
  resizeWindow: "Redimensionner la fen\xEAtre",
884
887
  styleTranscript: "Ouvrir les param\xE8tres de style de transcription",
885
888
  closeMenu: "Fermer le menu",
886
- styleTitle: "Style de Transcription"
889
+ styleTitle: "Style de Transcription",
890
+ autoscroll: "D\xE9filement automatique"
887
891
  },
888
892
  settings: {
889
893
  title: "Param\xE8tres",
@@ -1004,7 +1008,8 @@ var VidPly = (() => {
1004
1008
  resizeWindow: "\u30A6\u30A3\u30F3\u30C9\u30A6\u306E\u30B5\u30A4\u30BA\u5909\u66F4",
1005
1009
  styleTranscript: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB\u8A2D\u5B9A\u3092\u958B\u304F",
1006
1010
  closeMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u9589\u3058\u308B",
1007
- styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB"
1011
+ styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB",
1012
+ autoscroll: "\u81EA\u52D5\u30B9\u30AF\u30ED\u30FC\u30EB"
1008
1013
  },
1009
1014
  settings: {
1010
1015
  title: "\u8A2D\u5B9A",
@@ -1183,8 +1188,8 @@ var VidPly = (() => {
1183
1188
  language: `<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>`,
1184
1189
  hd: `<path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm7-1c0 .55-.45 1-1 1h-.75v1.5h-1.5V15H14c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v4zm-3.5-.5h2v-3h-2v3z"/>`,
1185
1190
  transcript: `<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>`,
1186
- audioDescription: `<rect x="2" y="5" width="20" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="currentColor">AD</text>`,
1187
- audioDescriptionOn: `<rect x="2" y="5" width="20" height="14" rx="2" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="#ffffff">AD</text>`,
1191
+ audioDescription: `<rect x="2" y="5" width="20" height="14" rx="2" fill="#ffffff" stroke="#ffffff" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="#1a1a1a">AD</text>`,
1192
+ audioDescriptionOn: `<rect x="2" y="5" width="20" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><text x="12" y="16" font-family="Arial, sans-serif" font-size="10" font-weight="bold" text-anchor="middle" fill="currentColor">AD</text>`,
1188
1193
  signLanguage: `<g transform="scale(1.5)"><path d="M16 11.3c-.1-.9-4.8 1.3-5.4 1.1-2.6-1 5.8-1.3 5.1-2.9s-5.1 1.5-6 1.4C6.5 9.4 16.5 9.1 13.5 8c-1.9-.6-8.8 2.9-6.8.4.7-.6.7-1.9-.7-1.7-9.7 7.2-.7 12.2 8.8 7 0-1.3-3.5.4-4.1.4-2.6 0 5.6-2 5.4-3ZM3.9 7.8c3.2-4.2 3.7 1.2 6 .1s.2-.2.2-.3c.7-2.7 2.5-7.5-1.5-1.3-1.6 0 1.1-4 1-4.6C8.9-1 7.3 4.4 7.2 4.9c-1.6.7-.9-1.4-.7-1.5 3-6-.6-3.1-.9.4-2.5 1.8 0-2.8 0-3.5C2.8-.9 4 9.4 1.1 4.9S.1 4.6 0 5c-.4 2.7 2.6 7.2 3.9 2.8Z"/></g>`,
1189
1194
  signLanguageOn: `<g transform="scale(1.5)"><path d="M16 11.3c-.1-.9-4.8 1.3-5.4 1.1-2.6-1 5.8-1.3 5.1-2.9s-5.1 1.5-6 1.4C6.5 9.4 16.5 9.1 13.5 8c-1.9-.6-8.8 2.9-6.8.4.7-.6.7-1.9-.7-1.7-9.7 7.2-.7 12.2 8.8 7 0-1.3-3.5.4-4.1.4-2.6 0 5.6-2 5.4-3ZM3.9 7.8c3.2-4.2 3.7 1.2 6 .1s.2-.2.2-.3c.7-2.7 2.5-7.5-1.5-1.3-1.6 0 1.1-4 1-4.6C8.9-1 7.3 4.4 7.2 4.9c-1.6.7-.9-1.4-.7-1.5 3-6-.6-3.1-.9.4-2.5 1.8 0-2.8 0-3.5C2.8-.9 4 9.4 1.1 4.9S.1 4.6 0 5c-.4 2.7 2.6 7.2 3.9 2.8Z"/></g>`,
1190
1195
  speaker: `<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>`,
@@ -3341,7 +3346,12 @@ var VidPly = (() => {
3341
3346
  this.styleDialog = null;
3342
3347
  this.styleDialogVisible = false;
3343
3348
  this.styleDialogJustOpened = false;
3349
+ this.languageSelector = null;
3350
+ this.currentTranscriptLanguage = null;
3351
+ this.availableTranscriptLanguages = [];
3352
+ this.languageSelectorHandler = null;
3344
3353
  const savedPreferences = this.storage.getTranscriptPreferences();
3354
+ this.autoscrollEnabled = (savedPreferences == null ? void 0 : savedPreferences.autoscroll) !== void 0 ? savedPreferences.autoscroll : true;
3345
3355
  this.transcriptStyle = {
3346
3356
  fontSize: (savedPreferences == null ? void 0 : savedPreferences.fontSize) || this.player.options.transcriptFontSize || "100%",
3347
3357
  fontFamily: (savedPreferences == null ? void 0 : savedPreferences.fontFamily) || this.player.options.transcriptFontFamily || "sans-serif",
@@ -3364,13 +3374,15 @@ var VidPly = (() => {
3364
3374
  documentClick: null,
3365
3375
  styleDialogKeydown: null
3366
3376
  };
3377
+ this.timeouts = /* @__PURE__ */ new Set();
3367
3378
  this.init();
3368
3379
  }
3369
3380
  init() {
3381
+ this.setupMetadataHandlingOnLoad();
3370
3382
  this.player.on("timeupdate", this.handlers.timeupdate);
3371
3383
  this.player.on("fullscreenchange", () => {
3372
3384
  if (this.isVisible) {
3373
- setTimeout(() => this.positionTranscript(), 100);
3385
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
3374
3386
  }
3375
3387
  });
3376
3388
  }
@@ -3391,7 +3403,7 @@ var VidPly = (() => {
3391
3403
  if (this.transcriptWindow) {
3392
3404
  this.transcriptWindow.style.display = "flex";
3393
3405
  this.isVisible = true;
3394
- setTimeout(() => {
3406
+ this.setManagedTimeout(() => {
3395
3407
  if (this.settingsButton) {
3396
3408
  this.settingsButton.focus();
3397
3409
  }
@@ -3402,8 +3414,8 @@ var VidPly = (() => {
3402
3414
  this.loadTranscriptData();
3403
3415
  if (this.transcriptWindow) {
3404
3416
  this.transcriptWindow.style.display = "flex";
3405
- setTimeout(() => this.positionTranscript(), 0);
3406
- setTimeout(() => {
3417
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
3418
+ this.setManagedTimeout(() => {
3407
3419
  if (this.settingsButton) {
3408
3420
  this.settingsButton.focus();
3409
3421
  }
@@ -3480,8 +3492,41 @@ var VidPly = (() => {
3480
3492
  const title = DOMUtils.createElement("h3", {
3481
3493
  textContent: i18n.t("transcript.title")
3482
3494
  });
3495
+ const autoscrollLabel = DOMUtils.createElement("label", {
3496
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
3497
+ attributes: {
3498
+ "title": i18n.t("transcript.autoscroll")
3499
+ }
3500
+ });
3501
+ this.autoscrollCheckbox = DOMUtils.createElement("input", {
3502
+ attributes: {
3503
+ "type": "checkbox",
3504
+ "checked": this.autoscrollEnabled,
3505
+ "aria-label": i18n.t("transcript.autoscroll")
3506
+ }
3507
+ });
3508
+ const autoscrollText = DOMUtils.createElement("span", {
3509
+ textContent: i18n.t("transcript.autoscroll"),
3510
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
3511
+ });
3512
+ autoscrollLabel.appendChild(this.autoscrollCheckbox);
3513
+ autoscrollLabel.appendChild(autoscrollText);
3514
+ this.autoscrollCheckbox.addEventListener("change", (e) => {
3515
+ this.autoscrollEnabled = e.target.checked;
3516
+ this.saveAutoscrollPreference();
3517
+ });
3483
3518
  this.headerLeft.appendChild(this.settingsButton);
3484
3519
  this.headerLeft.appendChild(title);
3520
+ this.headerLeft.appendChild(autoscrollLabel);
3521
+ this.languageSelector = DOMUtils.createElement("select", {
3522
+ className: `${this.player.options.classPrefix}-transcript-language-select`,
3523
+ attributes: {
3524
+ "aria-label": i18n.t("settings.language") || "Language",
3525
+ "style": "display: none;"
3526
+ // Hidden until we detect multiple languages
3527
+ }
3528
+ });
3529
+ this.headerLeft.appendChild(this.languageSelector);
3485
3530
  const closeButton = DOMUtils.createElement("button", {
3486
3531
  className: `${this.player.options.classPrefix}-transcript-close`,
3487
3532
  attributes: {
@@ -3524,8 +3569,10 @@ var VidPly = (() => {
3524
3569
  this.documentClickHandlerAdded = false;
3525
3570
  let resizeTimeout;
3526
3571
  this.handlers.resize = () => {
3527
- clearTimeout(resizeTimeout);
3528
- resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
3572
+ if (resizeTimeout) {
3573
+ this.clearManagedTimeout(resizeTimeout);
3574
+ }
3575
+ resizeTimeout = this.setManagedTimeout(() => this.positionTranscript(), 100);
3529
3576
  };
3530
3577
  window.addEventListener("resize", this.handlers.resize);
3531
3578
  }
@@ -3595,17 +3642,94 @@ var VidPly = (() => {
3595
3642
  }
3596
3643
  }
3597
3644
  }
3645
+ /**
3646
+ * Get available transcript languages from tracks
3647
+ */
3648
+ getAvailableTranscriptLanguages() {
3649
+ const textTracks = this.player.textTracks;
3650
+ const languages = /* @__PURE__ */ new Map();
3651
+ textTracks.forEach((track) => {
3652
+ if ((track.kind === "captions" || track.kind === "subtitles") && track.language) {
3653
+ if (!languages.has(track.language)) {
3654
+ languages.set(track.language, {
3655
+ language: track.language,
3656
+ label: track.label || track.language,
3657
+ track
3658
+ });
3659
+ }
3660
+ }
3661
+ });
3662
+ return Array.from(languages.values());
3663
+ }
3664
+ /**
3665
+ * Update language selector dropdown
3666
+ */
3667
+ updateLanguageSelector() {
3668
+ if (!this.languageSelector) return;
3669
+ this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
3670
+ this.languageSelector.innerHTML = "";
3671
+ if (this.availableTranscriptLanguages.length < 2) {
3672
+ this.languageSelector.style.display = "none";
3673
+ return;
3674
+ }
3675
+ this.languageSelector.style.display = "block";
3676
+ this.availableTranscriptLanguages.forEach((langInfo, index) => {
3677
+ const option = DOMUtils.createElement("option", {
3678
+ textContent: langInfo.label,
3679
+ attributes: {
3680
+ "value": langInfo.language
3681
+ }
3682
+ });
3683
+ this.languageSelector.appendChild(option);
3684
+ });
3685
+ if (this.currentTranscriptLanguage) {
3686
+ this.languageSelector.value = this.currentTranscriptLanguage;
3687
+ } else if (this.availableTranscriptLanguages.length > 0) {
3688
+ const activeTrack = this.player.textTracks.find(
3689
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.mode === "showing"
3690
+ );
3691
+ this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
3692
+ this.languageSelector.value = this.currentTranscriptLanguage;
3693
+ }
3694
+ if (this.languageSelectorHandler) {
3695
+ this.languageSelector.removeEventListener("change", this.languageSelectorHandler);
3696
+ }
3697
+ this.languageSelectorHandler = (e) => {
3698
+ this.currentTranscriptLanguage = e.target.value;
3699
+ this.loadTranscriptData();
3700
+ };
3701
+ this.languageSelector.addEventListener("change", this.languageSelectorHandler);
3702
+ }
3598
3703
  /**
3599
3704
  * Load transcript data from caption/subtitle tracks
3600
3705
  */
3601
3706
  loadTranscriptData() {
3602
3707
  this.transcriptEntries = [];
3603
3708
  this.transcriptContent.innerHTML = "";
3604
- const textTracks = Array.from(this.player.element.textTracks);
3605
- const captionTrack = textTracks.find(
3606
- (track) => track.kind === "captions" || track.kind === "subtitles"
3607
- );
3608
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3709
+ const textTracks = this.player.textTracks;
3710
+ let captionTrack = null;
3711
+ if (this.currentTranscriptLanguage) {
3712
+ captionTrack = textTracks.find(
3713
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.language === this.currentTranscriptLanguage
3714
+ );
3715
+ }
3716
+ if (!captionTrack) {
3717
+ captionTrack = textTracks.find(
3718
+ (track) => track.kind === "captions" || track.kind === "subtitles"
3719
+ );
3720
+ if (captionTrack) {
3721
+ this.currentTranscriptLanguage = captionTrack.language;
3722
+ }
3723
+ }
3724
+ let descriptionTrack = null;
3725
+ if (this.currentTranscriptLanguage) {
3726
+ descriptionTrack = textTracks.find(
3727
+ (track) => track.kind === "descriptions" && track.language === this.currentTranscriptLanguage
3728
+ );
3729
+ }
3730
+ if (!descriptionTrack) {
3731
+ descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3732
+ }
3609
3733
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3610
3734
  if (!captionTrack && !descriptionTrack && !metadataTrack) {
3611
3735
  this.showNoTranscriptMessage();
@@ -3634,7 +3758,7 @@ var VidPly = (() => {
3634
3758
  tracksToLoad.forEach((track) => {
3635
3759
  track.addEventListener("load", onLoad, { once: true });
3636
3760
  });
3637
- setTimeout(() => {
3761
+ this.setManagedTimeout(() => {
3638
3762
  this.loadTranscriptData();
3639
3763
  }, 500);
3640
3764
  return;
@@ -3667,24 +3791,61 @@ var VidPly = (() => {
3667
3791
  this.transcriptContent.appendChild(entry);
3668
3792
  });
3669
3793
  this.applyTranscriptStyles();
3794
+ this.updateLanguageSelector();
3795
+ }
3796
+ /**
3797
+ * Setup metadata handling on player load
3798
+ * This runs independently of transcript loading
3799
+ */
3800
+ setupMetadataHandlingOnLoad() {
3801
+ const setupMetadata = () => {
3802
+ const textTracks = this.player.textTracks;
3803
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3804
+ if (metadataTrack) {
3805
+ if (metadataTrack.mode === "disabled") {
3806
+ metadataTrack.mode = "hidden";
3807
+ }
3808
+ if (this.metadataCueChangeHandler) {
3809
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
3810
+ }
3811
+ this.metadataCueChangeHandler = () => {
3812
+ const activeCues = Array.from(metadataTrack.activeCues || []);
3813
+ if (activeCues.length > 0) {
3814
+ if (this.player.options.debug) {
3815
+ console.log("[VidPly Metadata] Active cues:", activeCues.map((c) => ({
3816
+ start: c.startTime,
3817
+ end: c.endTime,
3818
+ text: c.text
3819
+ })));
3820
+ }
3821
+ }
3822
+ activeCues.forEach((cue) => {
3823
+ this.handleMetadataCue(cue);
3824
+ });
3825
+ };
3826
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
3827
+ if (this.player.options.debug) {
3828
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
3829
+ console.log("[VidPly Metadata] Track enabled,", cueCount, "cues available");
3830
+ }
3831
+ } else if (this.player.options.debug) {
3832
+ console.warn("[VidPly Metadata] No metadata track found");
3833
+ }
3834
+ };
3835
+ setupMetadata();
3836
+ this.player.on("loadedmetadata", setupMetadata);
3670
3837
  }
3671
3838
  /**
3672
3839
  * Setup metadata handling
3673
3840
  * Metadata cues are not displayed but can be used programmatically
3841
+ * This is called when transcript data is loaded (for storing cues)
3674
3842
  */
3675
3843
  setupMetadataHandling() {
3676
3844
  if (!this.metadataCues || this.metadataCues.length === 0) {
3677
3845
  return;
3678
3846
  }
3679
- const textTracks = Array.from(this.player.element.textTracks);
3680
- const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3681
- if (metadataTrack) {
3682
- metadataTrack.addEventListener("cuechange", () => {
3683
- const activeCues = Array.from(metadataTrack.activeCues || []);
3684
- activeCues.forEach((cue) => {
3685
- this.handleMetadataCue(cue);
3686
- });
3687
- });
3847
+ if (this.player.options.debug) {
3848
+ console.log("[VidPly Metadata]", this.metadataCues.length, "cues stored from transcript load");
3688
3849
  }
3689
3850
  }
3690
3851
  /**
@@ -3693,6 +3854,12 @@ var VidPly = (() => {
3693
3854
  */
3694
3855
  handleMetadataCue(cue) {
3695
3856
  const text = cue.text.trim();
3857
+ if (this.player.options.debug) {
3858
+ console.log("[VidPly Metadata] Processing cue:", {
3859
+ time: cue.startTime,
3860
+ text
3861
+ });
3862
+ }
3696
3863
  this.player.emit("metadata", {
3697
3864
  time: cue.startTime,
3698
3865
  endTime: cue.endTime,
@@ -3700,18 +3867,40 @@ var VidPly = (() => {
3700
3867
  cue
3701
3868
  });
3702
3869
  if (text.includes("PAUSE")) {
3870
+ if (!this.player.state.paused) {
3871
+ if (this.player.options.debug) {
3872
+ console.log("[VidPly Metadata] Pausing video at", cue.startTime);
3873
+ }
3874
+ this.player.pause();
3875
+ }
3703
3876
  this.player.emit("metadata:pause", { time: cue.startTime, text });
3704
3877
  }
3705
3878
  const focusMatch = text.match(/FOCUS:([\w#-]+)/);
3706
3879
  if (focusMatch) {
3880
+ const targetSelector = focusMatch[1];
3881
+ const targetElement = document.querySelector(targetSelector);
3882
+ if (targetElement) {
3883
+ if (this.player.options.debug) {
3884
+ console.log("[VidPly Metadata] Focusing element:", targetSelector);
3885
+ }
3886
+ this.setManagedTimeout(() => {
3887
+ targetElement.focus();
3888
+ }, 10);
3889
+ } else if (this.player.options.debug) {
3890
+ console.warn("[VidPly Metadata] Element not found:", targetSelector);
3891
+ }
3707
3892
  this.player.emit("metadata:focus", {
3708
3893
  time: cue.startTime,
3709
- target: focusMatch[1],
3894
+ target: targetSelector,
3895
+ element: targetElement,
3710
3896
  text
3711
3897
  });
3712
3898
  }
3713
3899
  const hashtags = text.match(/#[\w-]+/g);
3714
3900
  if (hashtags) {
3901
+ if (this.player.options.debug) {
3902
+ console.log("[VidPly Metadata] Hashtags found:", hashtags);
3903
+ }
3715
3904
  this.player.emit("metadata:hashtags", {
3716
3905
  time: cue.startTime,
3717
3906
  hashtags,
@@ -3805,7 +3994,7 @@ var VidPly = (() => {
3805
3994
  * Scroll transcript window to show active entry
3806
3995
  */
3807
3996
  scrollToEntry(entryElement) {
3808
- if (!this.transcriptContent) return;
3997
+ if (!this.transcriptContent || !this.autoscrollEnabled) return;
3809
3998
  const contentRect = this.transcriptContent.getBoundingClientRect();
3810
3999
  const entryRect = entryElement.getBoundingClientRect();
3811
4000
  if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
@@ -3816,6 +4005,14 @@ var VidPly = (() => {
3816
4005
  });
3817
4006
  }
3818
4007
  }
4008
+ /**
4009
+ * Save autoscroll preference to localStorage
4010
+ */
4011
+ saveAutoscrollPreference() {
4012
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
4013
+ savedPreferences.autoscroll = this.autoscrollEnabled;
4014
+ this.storage.saveTranscriptPreferences(savedPreferences);
4015
+ }
3819
4016
  /**
3820
4017
  * Setup drag and drop functionality
3821
4018
  */
@@ -3828,6 +4025,9 @@ var VidPly = (() => {
3828
4025
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3829
4026
  return;
3830
4027
  }
4028
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
4029
+ return;
4030
+ }
3831
4031
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3832
4032
  return;
3833
4033
  }
@@ -3854,6 +4054,9 @@ var VidPly = (() => {
3854
4054
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3855
4055
  return;
3856
4056
  }
4057
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
4058
+ return;
4059
+ }
3857
4060
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3858
4061
  return;
3859
4062
  }
@@ -4608,6 +4811,30 @@ var VidPly = (() => {
4608
4811
  entry.style.fontFamily = this.transcriptStyle.fontFamily;
4609
4812
  });
4610
4813
  }
4814
+ /**
4815
+ * Set a managed timeout that will be cleaned up on destroy
4816
+ * @param {Function} callback - Callback function
4817
+ * @param {number} delay - Delay in milliseconds
4818
+ * @returns {number} Timeout ID
4819
+ */
4820
+ setManagedTimeout(callback, delay) {
4821
+ const timeoutId = setTimeout(() => {
4822
+ this.timeouts.delete(timeoutId);
4823
+ callback();
4824
+ }, delay);
4825
+ this.timeouts.add(timeoutId);
4826
+ return timeoutId;
4827
+ }
4828
+ /**
4829
+ * Clear a managed timeout
4830
+ * @param {number} timeoutId - Timeout ID to clear
4831
+ */
4832
+ clearManagedTimeout(timeoutId) {
4833
+ if (timeoutId) {
4834
+ clearTimeout(timeoutId);
4835
+ this.timeouts.delete(timeoutId);
4836
+ }
4837
+ }
4611
4838
  /**
4612
4839
  * Cleanup
4613
4840
  */
@@ -4661,6 +4888,8 @@ var VidPly = (() => {
4661
4888
  if (this.handlers.resize) {
4662
4889
  window.removeEventListener("resize", this.handlers.resize);
4663
4890
  }
4891
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
4892
+ this.timeouts.clear();
4664
4893
  this.handlers = null;
4665
4894
  if (this.transcriptWindow && this.transcriptWindow.parentNode) {
4666
4895
  this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
@@ -5332,7 +5561,7 @@ var VidPly = (() => {
5332
5561
  };
5333
5562
 
5334
5563
  // src/core/Player.js
5335
- var Player = class extends EventEmitter {
5564
+ var Player = class _Player extends EventEmitter {
5336
5565
  constructor(element, options = {}) {
5337
5566
  super();
5338
5567
  this.element = typeof element === "string" ? document.querySelector(element) : element;
@@ -5440,6 +5669,8 @@ var VidPly = (() => {
5440
5669
  screenReaderAnnouncements: true,
5441
5670
  highContrast: false,
5442
5671
  focusHighlight: true,
5672
+ metadataAlerts: {},
5673
+ metadataHashtags: {},
5443
5674
  // Languages
5444
5675
  language: "en",
5445
5676
  languages: ["en"],
@@ -5458,6 +5689,8 @@ var VidPly = (() => {
5458
5689
  onError: null,
5459
5690
  ...options
5460
5691
  };
5692
+ this.options.metadataAlerts = this.options.metadataAlerts || {};
5693
+ this.options.metadataHashtags = this.options.metadataHashtags || {};
5461
5694
  this.storage = new StorageManager("vidply");
5462
5695
  const savedPrefs = this.storage.getPlayerPreferences();
5463
5696
  if (savedPrefs) {
@@ -5489,12 +5722,24 @@ var VidPly = (() => {
5489
5722
  this.audioDescriptionSrc = this.options.audioDescriptionSrc;
5490
5723
  this.signLanguageSrc = this.options.signLanguageSrc;
5491
5724
  this.signLanguageVideo = null;
5725
+ this.audioDescriptionSourceElement = null;
5726
+ this.originalAudioDescriptionSource = null;
5727
+ this.audioDescriptionCaptionTracks = [];
5728
+ this._textTracksCache = null;
5729
+ this._textTracksDirty = true;
5730
+ this._sourceElementsCache = null;
5731
+ this._sourceElementsDirty = true;
5732
+ this._trackElementsCache = null;
5733
+ this._trackElementsDirty = true;
5734
+ this.timeouts = /* @__PURE__ */ new Set();
5492
5735
  this.container = null;
5493
5736
  this.renderer = null;
5494
5737
  this.controlBar = null;
5495
5738
  this.captionManager = null;
5496
5739
  this.keyboardManager = null;
5497
5740
  this.settingsDialog = null;
5741
+ this.metadataCueChangeHandler = null;
5742
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5498
5743
  this.init();
5499
5744
  }
5500
5745
  async init() {
@@ -5526,6 +5771,7 @@ var VidPly = (() => {
5526
5771
  if (this.options.transcript || this.options.transcriptButton) {
5527
5772
  this.transcriptManager = new TranscriptManager(this);
5528
5773
  }
5774
+ this.setupMetadataHandling();
5529
5775
  if (this.options.keyboard) {
5530
5776
  this.keyboardManager = new KeyboardManager(this);
5531
5777
  }
@@ -5611,6 +5857,8 @@ var VidPly = (() => {
5611
5857
  if (this.element.tagName === "VIDEO") {
5612
5858
  this.createPlayButtonOverlay();
5613
5859
  }
5860
+ this.element.vidply = this;
5861
+ _Player.instances.push(this);
5614
5862
  this.element.style.cursor = "pointer";
5615
5863
  this.element.addEventListener("click", (e) => {
5616
5864
  if (e.target === this.element) {
@@ -5643,6 +5891,53 @@ var VidPly = (() => {
5643
5891
  if (!src) {
5644
5892
  throw new Error("No media source found");
5645
5893
  }
5894
+ const sourceElements = this.sourceElements;
5895
+ for (const sourceEl of sourceElements) {
5896
+ const descSrc = sourceEl.getAttribute("data-desc-src");
5897
+ const origSrc = sourceEl.getAttribute("data-orig-src");
5898
+ if (descSrc || origSrc) {
5899
+ if (!this.audioDescriptionSourceElement) {
5900
+ this.audioDescriptionSourceElement = sourceEl;
5901
+ }
5902
+ if (origSrc) {
5903
+ if (!this.originalAudioDescriptionSource) {
5904
+ this.originalAudioDescriptionSource = origSrc;
5905
+ }
5906
+ if (!this.originalSrc) {
5907
+ this.originalSrc = origSrc;
5908
+ }
5909
+ } else {
5910
+ const currentSrcAttr = sourceEl.getAttribute("src");
5911
+ if (!this.originalAudioDescriptionSource && currentSrcAttr) {
5912
+ this.originalAudioDescriptionSource = currentSrcAttr;
5913
+ }
5914
+ if (!this.originalSrc && currentSrcAttr) {
5915
+ this.originalSrc = currentSrcAttr;
5916
+ }
5917
+ }
5918
+ if (descSrc && !this.audioDescriptionSrc) {
5919
+ this.audioDescriptionSrc = descSrc;
5920
+ }
5921
+ }
5922
+ }
5923
+ const trackElements = this.trackElements;
5924
+ trackElements.forEach((trackEl) => {
5925
+ const trackKind = trackEl.getAttribute("kind");
5926
+ const trackDescSrc = trackEl.getAttribute("data-desc-src");
5927
+ if (trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters") {
5928
+ if (trackDescSrc) {
5929
+ this.audioDescriptionCaptionTracks.push({
5930
+ trackElement: trackEl,
5931
+ originalSrc: trackEl.getAttribute("src"),
5932
+ describedSrc: trackDescSrc,
5933
+ originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
5934
+ explicit: true
5935
+ // Explicitly defined, so we should validate it
5936
+ });
5937
+ this.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute("src")} -> ${trackDescSrc}`);
5938
+ }
5939
+ }
5940
+ });
5646
5941
  if (!this.originalSrc) {
5647
5942
  this.originalSrc = src;
5648
5943
  }
@@ -5659,6 +5954,106 @@ var VidPly = (() => {
5659
5954
  this.log(`Using ${renderer.name} renderer`);
5660
5955
  this.renderer = new renderer(this);
5661
5956
  await this.renderer.init();
5957
+ this.invalidateTrackCache();
5958
+ }
5959
+ /**
5960
+ * Get cached text tracks array
5961
+ * @returns {Array} Array of text tracks
5962
+ */
5963
+ get textTracks() {
5964
+ if (!this._textTracksCache || this._textTracksDirty) {
5965
+ this._textTracksCache = Array.from(this.element.textTracks || []);
5966
+ this._textTracksDirty = false;
5967
+ }
5968
+ return this._textTracksCache;
5969
+ }
5970
+ /**
5971
+ * Get cached source elements array
5972
+ * @returns {Array} Array of source elements
5973
+ */
5974
+ get sourceElements() {
5975
+ if (!this._sourceElementsCache || this._sourceElementsDirty) {
5976
+ this._sourceElementsCache = Array.from(this.element.querySelectorAll("source"));
5977
+ this._sourceElementsDirty = false;
5978
+ }
5979
+ return this._sourceElementsCache;
5980
+ }
5981
+ /**
5982
+ * Get cached track elements array
5983
+ * @returns {Array} Array of track elements
5984
+ */
5985
+ get trackElements() {
5986
+ if (!this._trackElementsCache || this._trackElementsDirty) {
5987
+ this._trackElementsCache = Array.from(this.element.querySelectorAll("track"));
5988
+ this._trackElementsDirty = false;
5989
+ }
5990
+ return this._trackElementsCache;
5991
+ }
5992
+ /**
5993
+ * Invalidate DOM query cache (call when tracks/sources change)
5994
+ */
5995
+ invalidateTrackCache() {
5996
+ this._textTracksDirty = true;
5997
+ this._trackElementsDirty = true;
5998
+ this._sourceElementsDirty = true;
5999
+ }
6000
+ /**
6001
+ * Find a text track by kind and optionally language
6002
+ * @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
6003
+ * @param {string} [language] - Optional language code
6004
+ * @returns {TextTrack|null} Found track or null
6005
+ */
6006
+ findTextTrack(kind, language = null) {
6007
+ const tracks = this.textTracks;
6008
+ if (language) {
6009
+ return tracks.find((t) => t.kind === kind && t.language === language);
6010
+ }
6011
+ return tracks.find((t) => t.kind === kind);
6012
+ }
6013
+ /**
6014
+ * Find a source element by attribute
6015
+ * @param {string} attribute - Attribute name (e.g., 'data-desc-src')
6016
+ * @param {string} [value] - Optional attribute value
6017
+ * @returns {Element|null} Found source element or null
6018
+ */
6019
+ findSourceElement(attribute, value = null) {
6020
+ const sources = this.sourceElements;
6021
+ if (value) {
6022
+ return sources.find((el) => el.getAttribute(attribute) === value);
6023
+ }
6024
+ return sources.find((el) => el.hasAttribute(attribute));
6025
+ }
6026
+ /**
6027
+ * Find a track element by its associated TextTrack
6028
+ * @param {TextTrack} track - The TextTrack object
6029
+ * @returns {Element|null} Found track element or null
6030
+ */
6031
+ findTrackElement(track) {
6032
+ return this.trackElements.find((el) => el.track === track);
6033
+ }
6034
+ /**
6035
+ * Set a managed timeout that will be cleaned up on destroy
6036
+ * @param {Function} callback - Callback function
6037
+ * @param {number} delay - Delay in milliseconds
6038
+ * @returns {number} Timeout ID
6039
+ */
6040
+ setManagedTimeout(callback, delay) {
6041
+ const timeoutId = setTimeout(() => {
6042
+ this.timeouts.delete(timeoutId);
6043
+ callback();
6044
+ }, delay);
6045
+ this.timeouts.add(timeoutId);
6046
+ return timeoutId;
6047
+ }
6048
+ /**
6049
+ * Clear a managed timeout
6050
+ * @param {number} timeoutId - Timeout ID to clear
6051
+ */
6052
+ clearManagedTimeout(timeoutId) {
6053
+ if (timeoutId) {
6054
+ clearTimeout(timeoutId);
6055
+ this.timeouts.delete(timeoutId);
6056
+ }
5662
6057
  }
5663
6058
  /**
5664
6059
  * Load new media source (for playlists)
@@ -5674,8 +6069,9 @@ var VidPly = (() => {
5674
6069
  if (this.renderer) {
5675
6070
  this.pause();
5676
6071
  }
5677
- const existingTracks = this.element.querySelectorAll("track");
6072
+ const existingTracks = this.trackElements;
5678
6073
  existingTracks.forEach((track) => track.remove());
6074
+ this.invalidateTrackCache();
5679
6075
  this.element.src = config.src;
5680
6076
  if (config.type) {
5681
6077
  this.element.type = config.type;
@@ -5695,6 +6091,7 @@ var VidPly = (() => {
5695
6091
  }
5696
6092
  this.element.appendChild(track);
5697
6093
  });
6094
+ this.invalidateTrackCache();
5698
6095
  }
5699
6096
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5700
6097
  if (shouldChangeRenderer && this.renderer) {
@@ -5927,15 +6324,398 @@ var VidPly = (() => {
5927
6324
  this.enableCaptions();
5928
6325
  }
5929
6326
  }
6327
+ /**
6328
+ * Check if a track file exists
6329
+ * @param {string} url - Track file URL
6330
+ * @returns {Promise<boolean>} - True if file exists
6331
+ */
6332
+ async validateTrackExists(url) {
6333
+ try {
6334
+ const response = await fetch(url, { method: "HEAD", cache: "no-cache" });
6335
+ return response.ok;
6336
+ } catch (error) {
6337
+ return false;
6338
+ }
6339
+ }
5930
6340
  // Audio Description
5931
6341
  async enableAudioDescription() {
5932
- if (!this.audioDescriptionSrc) {
5933
- console.warn("VidPly: No audio description source provided");
6342
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6343
+ const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
6344
+ if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
6345
+ console.warn("VidPly: No audio description source, source elements, or tracks provided");
5934
6346
  return;
5935
6347
  }
5936
6348
  const currentTime = this.state.currentTime;
5937
6349
  const wasPlaying = this.state.playing;
5938
- this.element.src = this.audioDescriptionSrc;
6350
+ let swappedTracksForTranscript = [];
6351
+ if (this.audioDescriptionSourceElement) {
6352
+ const currentSrc = this.element.currentSrc || this.element.src;
6353
+ const sourceElements = this.sourceElements;
6354
+ let sourceElementToUpdate = null;
6355
+ let descSrc = this.audioDescriptionSrc;
6356
+ for (const sourceEl of sourceElements) {
6357
+ const sourceSrc = sourceEl.getAttribute("src");
6358
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6359
+ const sourceFilename = sourceSrc ? sourceSrc.split("/").pop() : "";
6360
+ const currentFilename = currentSrc ? currentSrc.split("/").pop() : "";
6361
+ if (currentSrc && (currentSrc === sourceSrc || currentSrc.includes(sourceSrc) || currentSrc.includes(sourceFilename) || sourceFilename && currentFilename === sourceFilename)) {
6362
+ sourceElementToUpdate = sourceEl;
6363
+ if (descSrcAttr) {
6364
+ descSrc = descSrcAttr;
6365
+ } else if (sourceSrc) {
6366
+ descSrc = this.audioDescriptionSrc || descSrc;
6367
+ }
6368
+ break;
6369
+ }
6370
+ }
6371
+ if (!sourceElementToUpdate) {
6372
+ sourceElementToUpdate = this.audioDescriptionSourceElement;
6373
+ const storedDescSrc = sourceElementToUpdate.getAttribute("data-desc-src");
6374
+ if (storedDescSrc) {
6375
+ descSrc = storedDescSrc;
6376
+ }
6377
+ }
6378
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6379
+ const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
6380
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
6381
+ if (trackInfo.explicit === true) {
6382
+ try {
6383
+ const exists = await this.validateTrackExists(trackInfo.describedSrc);
6384
+ return { trackInfo, exists };
6385
+ } catch (error) {
6386
+ return { trackInfo, exists: false };
6387
+ }
6388
+ } else {
6389
+ return { trackInfo, exists: false };
6390
+ }
6391
+ }
6392
+ return { trackInfo, exists: false };
6393
+ });
6394
+ const validationResults = await Promise.all(validationPromises);
6395
+ const tracksToSwap = validationResults.filter((result) => result.exists);
6396
+ if (tracksToSwap.length > 0) {
6397
+ const trackModes = /* @__PURE__ */ new Map();
6398
+ tracksToSwap.forEach(({ trackInfo }) => {
6399
+ const textTrack = trackInfo.trackElement.track;
6400
+ if (textTrack) {
6401
+ trackModes.set(trackInfo, {
6402
+ wasShowing: textTrack.mode === "showing",
6403
+ wasHidden: textTrack.mode === "hidden"
6404
+ });
6405
+ } else {
6406
+ trackModes.set(trackInfo, {
6407
+ wasShowing: false,
6408
+ wasHidden: false
6409
+ });
6410
+ }
6411
+ });
6412
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
6413
+ const oldSrc = trackInfo.trackElement.getAttribute("src");
6414
+ const parent = trackInfo.trackElement.parentNode;
6415
+ const nextSibling = trackInfo.trackElement.nextSibling;
6416
+ const attributes = {};
6417
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
6418
+ attributes[attr.name] = attr.value;
6419
+ });
6420
+ return {
6421
+ trackInfo,
6422
+ oldSrc,
6423
+ parent,
6424
+ nextSibling,
6425
+ attributes
6426
+ };
6427
+ });
6428
+ tracksToReadd.forEach(({ trackInfo }) => {
6429
+ trackInfo.trackElement.remove();
6430
+ });
6431
+ this.element.load();
6432
+ setTimeout(() => {
6433
+ tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
6434
+ swappedTracksForTranscript.push(trackInfo);
6435
+ const newTrackElement = document.createElement("track");
6436
+ newTrackElement.setAttribute("src", trackInfo.describedSrc);
6437
+ Object.keys(attributes).forEach((attrName) => {
6438
+ if (attrName !== "src" && attrName !== "data-desc-src") {
6439
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
6440
+ }
6441
+ });
6442
+ if (nextSibling && nextSibling.parentNode) {
6443
+ parent.insertBefore(newTrackElement, nextSibling);
6444
+ } else {
6445
+ parent.appendChild(newTrackElement);
6446
+ }
6447
+ trackInfo.trackElement = newTrackElement;
6448
+ });
6449
+ this.element.load();
6450
+ this.invalidateTrackCache();
6451
+ const setupNewTracks = () => {
6452
+ this.setManagedTimeout(() => {
6453
+ swappedTracksForTranscript.forEach((trackInfo) => {
6454
+ const trackElement = trackInfo.trackElement;
6455
+ const newTextTrack = trackElement.track;
6456
+ if (newTextTrack) {
6457
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
6458
+ newTextTrack.mode = "hidden";
6459
+ const restoreMode = () => {
6460
+ if (modeInfo.wasShowing) {
6461
+ newTextTrack.mode = "hidden";
6462
+ } else if (modeInfo.wasHidden) {
6463
+ newTextTrack.mode = "hidden";
6464
+ } else {
6465
+ newTextTrack.mode = "disabled";
6466
+ }
6467
+ };
6468
+ if (newTextTrack.readyState >= 2) {
6469
+ restoreMode();
6470
+ } else {
6471
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
6472
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
6473
+ }
6474
+ }
6475
+ });
6476
+ }, 300);
6477
+ };
6478
+ if (this.element.readyState >= 1) {
6479
+ setTimeout(setupNewTracks, 200);
6480
+ } else {
6481
+ this.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
6482
+ setTimeout(setupNewTracks, 2e3);
6483
+ }
6484
+ }, 100);
6485
+ const skippedCount = validationResults.length - tracksToSwap.length;
6486
+ }
6487
+ }
6488
+ const allSourceElements = this.sourceElements;
6489
+ const sourcesToUpdate = [];
6490
+ allSourceElements.forEach((sourceEl) => {
6491
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6492
+ const currentSrc2 = sourceEl.getAttribute("src");
6493
+ if (descSrcAttr) {
6494
+ const type = sourceEl.getAttribute("type");
6495
+ let origSrc = sourceEl.getAttribute("data-orig-src");
6496
+ if (!origSrc) {
6497
+ origSrc = currentSrc2;
6498
+ }
6499
+ sourcesToUpdate.push({
6500
+ src: descSrcAttr,
6501
+ // Use described version
6502
+ type,
6503
+ origSrc,
6504
+ descSrc: descSrcAttr
6505
+ });
6506
+ } else {
6507
+ const type = sourceEl.getAttribute("type");
6508
+ const src = sourceEl.getAttribute("src");
6509
+ sourcesToUpdate.push({
6510
+ src,
6511
+ type,
6512
+ origSrc: null,
6513
+ descSrc: null
6514
+ });
6515
+ }
6516
+ });
6517
+ allSourceElements.forEach((sourceEl) => {
6518
+ sourceEl.remove();
6519
+ });
6520
+ sourcesToUpdate.forEach((sourceInfo) => {
6521
+ const newSource = document.createElement("source");
6522
+ newSource.setAttribute("src", sourceInfo.src);
6523
+ if (sourceInfo.type) {
6524
+ newSource.setAttribute("type", sourceInfo.type);
6525
+ }
6526
+ if (sourceInfo.origSrc) {
6527
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6528
+ }
6529
+ if (sourceInfo.descSrc) {
6530
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6531
+ }
6532
+ this.element.appendChild(newSource);
6533
+ });
6534
+ this.element.load();
6535
+ await new Promise((resolve) => {
6536
+ const onLoadedMetadata = () => {
6537
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
6538
+ resolve();
6539
+ };
6540
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6541
+ });
6542
+ await new Promise((resolve) => setTimeout(resolve, 300));
6543
+ if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6544
+ if (this.element.readyState >= 1) {
6545
+ this.element.currentTime = 1e-3;
6546
+ setTimeout(() => {
6547
+ this.element.currentTime = 0;
6548
+ }, 10);
6549
+ }
6550
+ }
6551
+ this.seek(currentTime);
6552
+ if (wasPlaying) {
6553
+ this.play();
6554
+ }
6555
+ this.state.audioDescriptionEnabled = true;
6556
+ this.emit("audiodescriptionenabled");
6557
+ } else {
6558
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6559
+ const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
6560
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
6561
+ if (trackInfo.explicit === true) {
6562
+ try {
6563
+ const exists = await this.validateTrackExists(trackInfo.describedSrc);
6564
+ return { trackInfo, exists };
6565
+ } catch (error) {
6566
+ return { trackInfo, exists: false };
6567
+ }
6568
+ } else {
6569
+ return { trackInfo, exists: false };
6570
+ }
6571
+ }
6572
+ return { trackInfo, exists: false };
6573
+ });
6574
+ const validationResults = await Promise.all(validationPromises);
6575
+ const tracksToSwap = validationResults.filter((result) => result.exists);
6576
+ if (tracksToSwap.length > 0) {
6577
+ const trackModes = /* @__PURE__ */ new Map();
6578
+ tracksToSwap.forEach(({ trackInfo }) => {
6579
+ const textTrack = trackInfo.trackElement.track;
6580
+ if (textTrack) {
6581
+ trackModes.set(trackInfo, {
6582
+ wasShowing: textTrack.mode === "showing",
6583
+ wasHidden: textTrack.mode === "hidden"
6584
+ });
6585
+ } else {
6586
+ trackModes.set(trackInfo, {
6587
+ wasShowing: false,
6588
+ wasHidden: false
6589
+ });
6590
+ }
6591
+ });
6592
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
6593
+ const oldSrc = trackInfo.trackElement.getAttribute("src");
6594
+ const parent = trackInfo.trackElement.parentNode;
6595
+ const nextSibling = trackInfo.trackElement.nextSibling;
6596
+ const attributes = {};
6597
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
6598
+ attributes[attr.name] = attr.value;
6599
+ });
6600
+ return {
6601
+ trackInfo,
6602
+ oldSrc,
6603
+ parent,
6604
+ nextSibling,
6605
+ attributes
6606
+ };
6607
+ });
6608
+ tracksToReadd.forEach(({ trackInfo }) => {
6609
+ trackInfo.trackElement.remove();
6610
+ });
6611
+ this.element.load();
6612
+ setTimeout(() => {
6613
+ tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
6614
+ swappedTracksForTranscript.push(trackInfo);
6615
+ const newTrackElement = document.createElement("track");
6616
+ newTrackElement.setAttribute("src", trackInfo.describedSrc);
6617
+ Object.keys(attributes).forEach((attrName) => {
6618
+ if (attrName !== "src" && attrName !== "data-desc-src") {
6619
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
6620
+ }
6621
+ });
6622
+ if (nextSibling && nextSibling.parentNode) {
6623
+ parent.insertBefore(newTrackElement, nextSibling);
6624
+ } else {
6625
+ parent.appendChild(newTrackElement);
6626
+ }
6627
+ trackInfo.trackElement = newTrackElement;
6628
+ });
6629
+ this.element.load();
6630
+ const setupNewTracks = () => {
6631
+ setTimeout(() => {
6632
+ swappedTracksForTranscript.forEach((trackInfo) => {
6633
+ const trackElement = trackInfo.trackElement;
6634
+ const newTextTrack = trackElement.track;
6635
+ if (newTextTrack) {
6636
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
6637
+ newTextTrack.mode = "hidden";
6638
+ const restoreMode = () => {
6639
+ if (modeInfo.wasShowing) {
6640
+ newTextTrack.mode = "hidden";
6641
+ } else if (modeInfo.wasHidden) {
6642
+ newTextTrack.mode = "hidden";
6643
+ } else {
6644
+ newTextTrack.mode = "disabled";
6645
+ }
6646
+ };
6647
+ if (newTextTrack.readyState >= 2) {
6648
+ restoreMode();
6649
+ } else {
6650
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
6651
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
6652
+ }
6653
+ }
6654
+ });
6655
+ }, 300);
6656
+ };
6657
+ if (this.element.readyState >= 1) {
6658
+ setTimeout(setupNewTracks, 200);
6659
+ } else {
6660
+ this.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
6661
+ setTimeout(setupNewTracks, 2e3);
6662
+ }
6663
+ }, 100);
6664
+ }
6665
+ }
6666
+ const fallbackSourceElements = this.sourceElements;
6667
+ const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6668
+ if (hasSourceElementsWithDesc2) {
6669
+ const fallbackSourcesToUpdate = [];
6670
+ fallbackSourceElements.forEach((sourceEl) => {
6671
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6672
+ const currentSrc = sourceEl.getAttribute("src");
6673
+ if (descSrcAttr) {
6674
+ const type = sourceEl.getAttribute("type");
6675
+ let origSrc = sourceEl.getAttribute("data-orig-src");
6676
+ if (!origSrc) {
6677
+ origSrc = currentSrc;
6678
+ }
6679
+ fallbackSourcesToUpdate.push({
6680
+ src: descSrcAttr,
6681
+ type,
6682
+ origSrc,
6683
+ descSrc: descSrcAttr
6684
+ });
6685
+ } else {
6686
+ const type = sourceEl.getAttribute("type");
6687
+ const src = sourceEl.getAttribute("src");
6688
+ fallbackSourcesToUpdate.push({
6689
+ src,
6690
+ type,
6691
+ origSrc: null,
6692
+ descSrc: null
6693
+ });
6694
+ }
6695
+ });
6696
+ fallbackSourceElements.forEach((sourceEl) => {
6697
+ sourceEl.remove();
6698
+ });
6699
+ fallbackSourcesToUpdate.forEach((sourceInfo) => {
6700
+ const newSource = document.createElement("source");
6701
+ newSource.setAttribute("src", sourceInfo.src);
6702
+ if (sourceInfo.type) {
6703
+ newSource.setAttribute("type", sourceInfo.type);
6704
+ }
6705
+ if (sourceInfo.origSrc) {
6706
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6707
+ }
6708
+ if (sourceInfo.descSrc) {
6709
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6710
+ }
6711
+ this.element.appendChild(newSource);
6712
+ });
6713
+ this.element.load();
6714
+ this.invalidateTrackCache();
6715
+ } else {
6716
+ this.element.src = this.audioDescriptionSrc;
6717
+ }
6718
+ }
5939
6719
  await new Promise((resolve) => {
5940
6720
  const onLoadedMetadata = () => {
5941
6721
  this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
@@ -5943,10 +6723,177 @@ var VidPly = (() => {
5943
6723
  };
5944
6724
  this.element.addEventListener("loadedmetadata", onLoadedMetadata);
5945
6725
  });
6726
+ if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6727
+ if (this.element.readyState >= 1) {
6728
+ this.element.currentTime = 1e-3;
6729
+ this.setManagedTimeout(() => {
6730
+ this.element.currentTime = 0;
6731
+ }, 10);
6732
+ }
6733
+ }
5946
6734
  this.seek(currentTime);
5947
6735
  if (wasPlaying) {
5948
6736
  this.play();
5949
6737
  }
6738
+ if (swappedTracksForTranscript.length > 0 && this.captionManager) {
6739
+ const wasCaptionsEnabled = this.state.captionsEnabled;
6740
+ let currentTrackInfo = null;
6741
+ if (this.captionManager.currentTrack) {
6742
+ const currentTrackIndex = this.captionManager.tracks.findIndex((t) => t.track === this.captionManager.currentTrack.track);
6743
+ if (currentTrackIndex >= 0) {
6744
+ currentTrackInfo = {
6745
+ language: this.captionManager.tracks[currentTrackIndex].language,
6746
+ kind: this.captionManager.tracks[currentTrackIndex].kind
6747
+ };
6748
+ }
6749
+ }
6750
+ setTimeout(() => {
6751
+ this.captionManager.tracks = [];
6752
+ this.captionManager.loadTracks();
6753
+ if (wasCaptionsEnabled && currentTrackInfo && this.captionManager.tracks.length > 0) {
6754
+ const matchingTrackIndex = this.captionManager.tracks.findIndex(
6755
+ (t) => t.language === currentTrackInfo.language && t.kind === currentTrackInfo.kind
6756
+ );
6757
+ if (matchingTrackIndex >= 0) {
6758
+ this.captionManager.enable(matchingTrackIndex);
6759
+ } else if (this.captionManager.tracks.length > 0) {
6760
+ this.captionManager.enable(0);
6761
+ }
6762
+ }
6763
+ }, 600);
6764
+ }
6765
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
6766
+ const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6767
+ if (swappedTracks.length > 0) {
6768
+ const onMetadataLoaded = () => {
6769
+ this.invalidateTrackCache();
6770
+ const allTextTracks = this.textTracks;
6771
+ const freshTracks = swappedTracks.map((trackInfo) => {
6772
+ const trackEl = trackInfo.trackElement;
6773
+ const expectedSrc = trackEl.getAttribute("src");
6774
+ const srclang = trackEl.getAttribute("srclang");
6775
+ const kind = trackEl.getAttribute("kind");
6776
+ let foundTrack = allTextTracks.find((track) => trackEl.track === track);
6777
+ if (!foundTrack) {
6778
+ foundTrack = allTextTracks.find((track) => {
6779
+ if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6780
+ const trackElementForTrack = this.findTrackElement(track);
6781
+ if (trackElementForTrack) {
6782
+ const actualSrc = trackElementForTrack.getAttribute("src");
6783
+ if (actualSrc === expectedSrc) {
6784
+ return true;
6785
+ }
6786
+ }
6787
+ }
6788
+ return false;
6789
+ });
6790
+ }
6791
+ if (foundTrack) {
6792
+ const trackElement = this.findTrackElement(foundTrack);
6793
+ if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6794
+ return null;
6795
+ }
6796
+ }
6797
+ return foundTrack;
6798
+ }).filter(Boolean);
6799
+ if (freshTracks.length === 0) {
6800
+ this.setManagedTimeout(() => {
6801
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6802
+ this.transcriptManager.loadTranscriptData();
6803
+ }
6804
+ }, 1e3);
6805
+ return;
6806
+ }
6807
+ freshTracks.forEach((track) => {
6808
+ if (track.mode === "disabled") {
6809
+ track.mode = "hidden";
6810
+ }
6811
+ });
6812
+ let loadedCount = 0;
6813
+ const checkLoaded = () => {
6814
+ loadedCount++;
6815
+ if (loadedCount >= freshTracks.length) {
6816
+ this.setManagedTimeout(() => {
6817
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6818
+ this.invalidateTrackCache();
6819
+ const allTextTracks2 = this.textTracks;
6820
+ const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6821
+ const hasCorrectTracks = freshTracks.some((track) => {
6822
+ const trackEl = this.findTrackElement(track);
6823
+ return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6824
+ });
6825
+ if (hasCorrectTracks || freshTracks.length > 0) {
6826
+ this.transcriptManager.loadTranscriptData();
6827
+ }
6828
+ }
6829
+ }, 800);
6830
+ }
6831
+ };
6832
+ freshTracks.forEach((track) => {
6833
+ if (track.mode === "disabled") {
6834
+ track.mode = "hidden";
6835
+ }
6836
+ const trackElementForTrack = this.findTrackElement(track);
6837
+ const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6838
+ const expectedTrackInfo = swappedTracks.find((t) => {
6839
+ const tEl = t.trackElement;
6840
+ return tEl && (tEl.track === track || tEl.getAttribute("srclang") === track.language && tEl.getAttribute("kind") === track.kind);
6841
+ });
6842
+ const expectedSrc = expectedTrackInfo ? expectedTrackInfo.describedSrc : null;
6843
+ if (expectedSrc && actualSrc && actualSrc !== expectedSrc) {
6844
+ checkLoaded();
6845
+ return;
6846
+ }
6847
+ if (track.readyState >= 2 && track.cues && track.cues.length > 0) {
6848
+ checkLoaded();
6849
+ } else {
6850
+ if (track.mode === "disabled") {
6851
+ track.mode = "hidden";
6852
+ }
6853
+ const onTrackLoad = () => {
6854
+ this.setManagedTimeout(checkLoaded, 300);
6855
+ };
6856
+ if (track.readyState >= 2) {
6857
+ this.setManagedTimeout(() => {
6858
+ if (track.cues && track.cues.length > 0) {
6859
+ checkLoaded();
6860
+ } else {
6861
+ track.addEventListener("load", onTrackLoad, { once: true });
6862
+ }
6863
+ }, 100);
6864
+ } else {
6865
+ track.addEventListener("load", onTrackLoad, { once: true });
6866
+ track.addEventListener("error", () => {
6867
+ checkLoaded();
6868
+ }, { once: true });
6869
+ }
6870
+ }
6871
+ });
6872
+ };
6873
+ const waitForTracks = () => {
6874
+ this.setManagedTimeout(() => {
6875
+ if (this.element.readyState >= 1) {
6876
+ onMetadataLoaded();
6877
+ } else {
6878
+ this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6879
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6880
+ }
6881
+ }, 500);
6882
+ };
6883
+ waitForTracks();
6884
+ setTimeout(() => {
6885
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6886
+ this.transcriptManager.loadTranscriptData();
6887
+ }
6888
+ }, 5e3);
6889
+ } else {
6890
+ setTimeout(() => {
6891
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6892
+ this.transcriptManager.loadTranscriptData();
6893
+ }
6894
+ }, 800);
6895
+ }
6896
+ }
5950
6897
  this.state.audioDescriptionEnabled = true;
5951
6898
  this.emit("audiodescriptionenabled");
5952
6899
  }
@@ -5956,7 +6903,64 @@ var VidPly = (() => {
5956
6903
  }
5957
6904
  const currentTime = this.state.currentTime;
5958
6905
  const wasPlaying = this.state.playing;
5959
- this.element.src = this.originalSrc;
6906
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6907
+ this.audioDescriptionCaptionTracks.forEach((trackInfo) => {
6908
+ if (trackInfo.trackElement && trackInfo.originalTrackSrc) {
6909
+ trackInfo.trackElement.setAttribute("src", trackInfo.originalTrackSrc);
6910
+ }
6911
+ });
6912
+ }
6913
+ const allSourceElements = this.sourceElements;
6914
+ const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6915
+ if (hasSourceElementsToSwap) {
6916
+ const sourcesToRestore = [];
6917
+ allSourceElements.forEach((sourceEl) => {
6918
+ const origSrcAttr = sourceEl.getAttribute("data-orig-src");
6919
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6920
+ if (origSrcAttr) {
6921
+ const type = sourceEl.getAttribute("type");
6922
+ sourcesToRestore.push({
6923
+ src: origSrcAttr,
6924
+ // Use original version
6925
+ type,
6926
+ origSrc: origSrcAttr,
6927
+ descSrc: descSrcAttr
6928
+ // Keep data-desc-src for future swaps
6929
+ });
6930
+ } else {
6931
+ const type = sourceEl.getAttribute("type");
6932
+ const src = sourceEl.getAttribute("src");
6933
+ sourcesToRestore.push({
6934
+ src,
6935
+ type,
6936
+ origSrc: null,
6937
+ descSrc: descSrcAttr
6938
+ });
6939
+ }
6940
+ });
6941
+ allSourceElements.forEach((sourceEl) => {
6942
+ sourceEl.remove();
6943
+ });
6944
+ sourcesToRestore.forEach((sourceInfo) => {
6945
+ const newSource = document.createElement("source");
6946
+ newSource.setAttribute("src", sourceInfo.src);
6947
+ if (sourceInfo.type) {
6948
+ newSource.setAttribute("type", sourceInfo.type);
6949
+ }
6950
+ if (sourceInfo.origSrc) {
6951
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6952
+ }
6953
+ if (sourceInfo.descSrc) {
6954
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6955
+ }
6956
+ this.element.appendChild(newSource);
6957
+ });
6958
+ this.element.load();
6959
+ } else {
6960
+ const originalSrcToUse = this.originalAudioDescriptionSource || this.originalSrc;
6961
+ this.element.src = originalSrcToUse;
6962
+ this.element.load();
6963
+ }
5960
6964
  await new Promise((resolve) => {
5961
6965
  const onLoadedMetadata = () => {
5962
6966
  this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
@@ -5968,13 +6972,50 @@ var VidPly = (() => {
5968
6972
  if (wasPlaying) {
5969
6973
  this.play();
5970
6974
  }
6975
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
6976
+ this.setManagedTimeout(() => {
6977
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6978
+ this.transcriptManager.loadTranscriptData();
6979
+ }
6980
+ }, 500);
6981
+ }
5971
6982
  this.state.audioDescriptionEnabled = false;
5972
6983
  this.emit("audiodescriptiondisabled");
5973
6984
  }
5974
6985
  async toggleAudioDescription() {
5975
- const textTracks = Array.from(this.element.textTracks || []);
5976
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
5977
- if (descriptionTrack) {
6986
+ const descriptionTrack = this.findTextTrack("descriptions");
6987
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6988
+ if (descriptionTrack && hasAudioDescriptionSrc) {
6989
+ if (this.state.audioDescriptionEnabled) {
6990
+ descriptionTrack.mode = "hidden";
6991
+ await this.disableAudioDescription();
6992
+ } else {
6993
+ await this.enableAudioDescription();
6994
+ const enableDescriptionTrack = () => {
6995
+ this.invalidateTrackCache();
6996
+ const descTrack = this.findTextTrack("descriptions");
6997
+ if (descTrack) {
6998
+ if (descTrack.mode === "disabled") {
6999
+ descTrack.mode = "hidden";
7000
+ this.setManagedTimeout(() => {
7001
+ descTrack.mode = "showing";
7002
+ }, 50);
7003
+ } else {
7004
+ descTrack.mode = "showing";
7005
+ }
7006
+ } else if (this.element.readyState < 2) {
7007
+ this.setManagedTimeout(enableDescriptionTrack, 100);
7008
+ }
7009
+ };
7010
+ if (this.element.readyState >= 1) {
7011
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7012
+ } else {
7013
+ this.element.addEventListener("loadedmetadata", () => {
7014
+ this.setManagedTimeout(enableDescriptionTrack, 200);
7015
+ }, { once: true });
7016
+ }
7017
+ }
7018
+ } else if (descriptionTrack) {
5978
7019
  if (descriptionTrack.mode === "showing") {
5979
7020
  descriptionTrack.mode = "hidden";
5980
7021
  this.state.audioDescriptionEnabled = false;
@@ -5984,7 +7025,7 @@ var VidPly = (() => {
5984
7025
  this.state.audioDescriptionEnabled = true;
5985
7026
  this.emit("audiodescriptionenabled");
5986
7027
  }
5987
- } else if (this.audioDescriptionSrc) {
7028
+ } else if (hasAudioDescriptionSrc) {
5988
7029
  if (this.state.audioDescriptionEnabled) {
5989
7030
  await this.disableAudioDescription();
5990
7031
  } else {
@@ -6385,9 +7426,25 @@ var VidPly = (() => {
6385
7426
  }
6386
7427
  }
6387
7428
  // Logging
6388
- log(message, type = "log") {
6389
- if (this.options.debug) {
6390
- console[type](`[VidPly]`, message);
7429
+ log(...messages) {
7430
+ if (!this.options.debug) {
7431
+ return;
7432
+ }
7433
+ let type = "log";
7434
+ if (messages.length > 0) {
7435
+ const potentialType = messages[messages.length - 1];
7436
+ if (typeof potentialType === "string" && console[potentialType]) {
7437
+ type = potentialType;
7438
+ messages = messages.slice(0, -1);
7439
+ }
7440
+ }
7441
+ if (messages.length === 0) {
7442
+ messages = [""];
7443
+ }
7444
+ if (typeof console[type] === "function") {
7445
+ console[type]("[VidPly]", ...messages);
7446
+ } else {
7447
+ console.log("[VidPly]", ...messages);
6391
7448
  }
6392
7449
  }
6393
7450
  // Setup responsive handlers
@@ -6447,7 +7504,7 @@ var VidPly = (() => {
6447
7504
  this.controlBar.updateFullscreenButton();
6448
7505
  }
6449
7506
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
6450
- setTimeout(() => {
7507
+ this.setManagedTimeout(() => {
6451
7508
  requestAnimationFrame(() => {
6452
7509
  this.storage.saveSignLanguagePreferences({ size: null });
6453
7510
  this.signLanguageDesiredPosition = "bottom-right";
@@ -6510,12 +7567,368 @@ var VidPly = (() => {
6510
7567
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
6511
7568
  this.fullscreenChangeHandler = null;
6512
7569
  }
7570
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
7571
+ this.timeouts.clear();
7572
+ if (this.metadataCueChangeHandler) {
7573
+ const textTracks = this.textTracks;
7574
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7575
+ if (metadataTrack) {
7576
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7577
+ }
7578
+ this.metadataCueChangeHandler = null;
7579
+ }
7580
+ if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
7581
+ this.metadataAlertHandlers.forEach(({ button, handler }) => {
7582
+ if (button && handler) {
7583
+ button.removeEventListener("click", handler);
7584
+ }
7585
+ });
7586
+ this.metadataAlertHandlers.clear();
7587
+ }
6513
7588
  if (this.container && this.container.parentNode) {
6514
7589
  this.container.parentNode.insertBefore(this.element, this.container);
6515
7590
  this.container.parentNode.removeChild(this.container);
6516
7591
  }
6517
7592
  this.removeAllListeners();
6518
7593
  }
7594
+ /**
7595
+ * Setup metadata track handling
7596
+ * This enables metadata tracks and listens for cue changes to trigger actions
7597
+ */
7598
+ setupMetadataHandling() {
7599
+ const setupMetadata = () => {
7600
+ const textTracks = this.textTracks;
7601
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7602
+ if (metadataTrack) {
7603
+ if (metadataTrack.mode === "disabled") {
7604
+ metadataTrack.mode = "hidden";
7605
+ }
7606
+ if (this.metadataCueChangeHandler) {
7607
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7608
+ }
7609
+ this.metadataCueChangeHandler = () => {
7610
+ const activeCues = Array.from(metadataTrack.activeCues || []);
7611
+ if (activeCues.length > 0) {
7612
+ if (this.options.debug) {
7613
+ this.log("[Metadata] Active cues:", activeCues.map((c) => ({
7614
+ start: c.startTime,
7615
+ end: c.endTime,
7616
+ text: c.text
7617
+ })));
7618
+ }
7619
+ }
7620
+ activeCues.forEach((cue) => {
7621
+ this.handleMetadataCue(cue);
7622
+ });
7623
+ };
7624
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
7625
+ if (this.options.debug) {
7626
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
7627
+ this.log("[Metadata] Track enabled,", cueCount, "cues available");
7628
+ }
7629
+ } else if (this.options.debug) {
7630
+ this.log("[Metadata] No metadata track found");
7631
+ }
7632
+ };
7633
+ setupMetadata();
7634
+ this.on("loadedmetadata", setupMetadata);
7635
+ }
7636
+ normalizeMetadataSelector(selector) {
7637
+ if (!selector) {
7638
+ return null;
7639
+ }
7640
+ const trimmed = selector.trim();
7641
+ if (!trimmed) {
7642
+ return null;
7643
+ }
7644
+ if (trimmed.startsWith("#") || trimmed.startsWith(".") || trimmed.startsWith("[")) {
7645
+ return trimmed;
7646
+ }
7647
+ return `#${trimmed}`;
7648
+ }
7649
+ resolveMetadataConfig(map, key) {
7650
+ if (!map || !key) {
7651
+ return null;
7652
+ }
7653
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
7654
+ return map[key];
7655
+ }
7656
+ const withoutHash = key.replace(/^#/, "");
7657
+ if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
7658
+ return map[withoutHash];
7659
+ }
7660
+ return null;
7661
+ }
7662
+ cacheMetadataAlertContent(element, config = {}) {
7663
+ if (!element) {
7664
+ return;
7665
+ }
7666
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7667
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7668
+ const titleEl = element.querySelector(titleSelector);
7669
+ if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
7670
+ titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
7671
+ }
7672
+ const messageEl = element.querySelector(messageSelector);
7673
+ if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
7674
+ messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
7675
+ }
7676
+ }
7677
+ restoreMetadataAlertContent(element, config = {}) {
7678
+ if (!element) {
7679
+ return;
7680
+ }
7681
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7682
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7683
+ const titleEl = element.querySelector(titleSelector);
7684
+ if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
7685
+ titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
7686
+ }
7687
+ const messageEl = element.querySelector(messageSelector);
7688
+ if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
7689
+ messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
7690
+ }
7691
+ }
7692
+ focusMetadataTarget(target, fallbackElement = null) {
7693
+ var _a, _b;
7694
+ if (!target || target === "none") {
7695
+ return;
7696
+ }
7697
+ if (target === "alert" && fallbackElement) {
7698
+ fallbackElement.focus();
7699
+ return;
7700
+ }
7701
+ if (target === "player") {
7702
+ if (this.container) {
7703
+ this.container.focus();
7704
+ }
7705
+ return;
7706
+ }
7707
+ if (target === "media") {
7708
+ this.element.focus();
7709
+ return;
7710
+ }
7711
+ if (target === "playButton") {
7712
+ const playButton = (_b = (_a = this.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.playPause;
7713
+ if (playButton) {
7714
+ playButton.focus();
7715
+ }
7716
+ return;
7717
+ }
7718
+ if (typeof target === "string") {
7719
+ const targetElement = document.querySelector(target);
7720
+ if (targetElement) {
7721
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
7722
+ targetElement.setAttribute("tabindex", "-1");
7723
+ }
7724
+ targetElement.focus();
7725
+ }
7726
+ }
7727
+ }
7728
+ handleMetadataAlert(selector, options = {}) {
7729
+ if (!selector) {
7730
+ return;
7731
+ }
7732
+ const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
7733
+ const element = options.element || document.querySelector(selector);
7734
+ if (!element) {
7735
+ if (this.options.debug) {
7736
+ this.log("[Metadata] Alert element not found:", selector);
7737
+ }
7738
+ return;
7739
+ }
7740
+ if (this.options.debug) {
7741
+ this.log("[Metadata] Handling alert", selector, { reason: options.reason, config });
7742
+ }
7743
+ this.cacheMetadataAlertContent(element, config);
7744
+ if (!element.dataset.vidplyAlertOriginalDisplay) {
7745
+ element.dataset.vidplyAlertOriginalDisplay = element.style.display || "";
7746
+ }
7747
+ if (!element.dataset.vidplyAlertDisplay) {
7748
+ element.dataset.vidplyAlertDisplay = config.display || "block";
7749
+ }
7750
+ const shouldShow = options.show !== void 0 ? options.show : config.show !== false;
7751
+ if (shouldShow) {
7752
+ const displayValue = config.display || element.dataset.vidplyAlertDisplay || "block";
7753
+ element.style.display = displayValue;
7754
+ element.hidden = false;
7755
+ element.removeAttribute("hidden");
7756
+ element.setAttribute("aria-hidden", "false");
7757
+ element.setAttribute("data-vidply-alert-active", "true");
7758
+ }
7759
+ const shouldReset = config.resetContent !== false && options.reason === "focus";
7760
+ if (shouldReset) {
7761
+ this.restoreMetadataAlertContent(element, config);
7762
+ }
7763
+ const shouldFocus = options.focus !== void 0 ? options.focus : config.focusOnShow ?? options.reason !== "focus";
7764
+ if (shouldShow && shouldFocus) {
7765
+ if (element.tabIndex === -1 && !element.hasAttribute("tabindex")) {
7766
+ element.setAttribute("tabindex", "-1");
7767
+ }
7768
+ element.focus();
7769
+ }
7770
+ if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
7771
+ element.scrollIntoView({ behavior: "smooth", block: "nearest" });
7772
+ }
7773
+ const continueSelector = config.continueButton;
7774
+ if (continueSelector) {
7775
+ let continueButton = null;
7776
+ if (continueSelector === "self") {
7777
+ continueButton = element;
7778
+ } else if (element.matches(continueSelector)) {
7779
+ continueButton = element;
7780
+ } else {
7781
+ continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
7782
+ }
7783
+ if (continueButton && !this.metadataAlertHandlers.has(selector)) {
7784
+ const handler = () => {
7785
+ const hideOnContinue = config.hideOnContinue !== false;
7786
+ if (hideOnContinue) {
7787
+ const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || "";
7788
+ element.style.display = config.hideDisplay || originalDisplay || "none";
7789
+ element.setAttribute("aria-hidden", "true");
7790
+ element.removeAttribute("data-vidply-alert-active");
7791
+ }
7792
+ if (config.resume !== false && this.state.paused) {
7793
+ this.play();
7794
+ }
7795
+ const focusTarget = config.focusTarget || "playButton";
7796
+ this.setManagedTimeout(() => {
7797
+ this.focusMetadataTarget(focusTarget, element);
7798
+ }, config.focusDelay ?? 100);
7799
+ };
7800
+ continueButton.addEventListener("click", handler);
7801
+ this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
7802
+ }
7803
+ }
7804
+ return element;
7805
+ }
7806
+ handleMetadataHashtags(hashtags) {
7807
+ if (!Array.isArray(hashtags) || hashtags.length === 0) {
7808
+ return;
7809
+ }
7810
+ const configMap = this.options.metadataHashtags;
7811
+ if (!configMap) {
7812
+ return;
7813
+ }
7814
+ hashtags.forEach((tag) => {
7815
+ const config = this.resolveMetadataConfig(configMap, tag);
7816
+ if (!config) {
7817
+ return;
7818
+ }
7819
+ const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
7820
+ if (!selector) {
7821
+ return;
7822
+ }
7823
+ const element = document.querySelector(selector);
7824
+ if (!element) {
7825
+ if (this.options.debug) {
7826
+ this.log("[Metadata] Hashtag target not found:", selector);
7827
+ }
7828
+ return;
7829
+ }
7830
+ if (this.options.debug) {
7831
+ this.log("[Metadata] Handling hashtag", tag, { selector, config });
7832
+ }
7833
+ this.cacheMetadataAlertContent(element, config);
7834
+ if (config.title) {
7835
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7836
+ const titleEl = element.querySelector(titleSelector);
7837
+ if (titleEl) {
7838
+ titleEl.textContent = config.title;
7839
+ }
7840
+ }
7841
+ if (config.message) {
7842
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7843
+ const messageEl = element.querySelector(messageSelector);
7844
+ if (messageEl) {
7845
+ messageEl.textContent = config.message;
7846
+ }
7847
+ }
7848
+ const show = config.show !== false;
7849
+ const focus = config.focus !== void 0 ? config.focus : false;
7850
+ this.handleMetadataAlert(selector, {
7851
+ element,
7852
+ show,
7853
+ focus,
7854
+ autoScroll: config.autoScroll,
7855
+ reason: "hashtag"
7856
+ });
7857
+ });
7858
+ }
7859
+ /**
7860
+ * Handle individual metadata cues
7861
+ * Parses metadata text and emits events or triggers actions
7862
+ */
7863
+ handleMetadataCue(cue) {
7864
+ const text = cue.text.trim();
7865
+ if (this.options.debug) {
7866
+ this.log("[Metadata] Processing cue:", {
7867
+ time: cue.startTime,
7868
+ text
7869
+ });
7870
+ }
7871
+ this.emit("metadata", {
7872
+ time: cue.startTime,
7873
+ endTime: cue.endTime,
7874
+ text,
7875
+ cue
7876
+ });
7877
+ if (text.includes("PAUSE")) {
7878
+ if (!this.state.paused) {
7879
+ if (this.options.debug) {
7880
+ this.log("[Metadata] Pausing video at", cue.startTime);
7881
+ }
7882
+ this.pause();
7883
+ }
7884
+ this.emit("metadata:pause", { time: cue.startTime, text });
7885
+ }
7886
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
7887
+ if (focusMatch) {
7888
+ const targetSelector = focusMatch[1];
7889
+ const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
7890
+ const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
7891
+ if (targetElement) {
7892
+ if (this.options.debug) {
7893
+ this.log("[Metadata] Focusing element:", normalizedSelector);
7894
+ }
7895
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
7896
+ targetElement.setAttribute("tabindex", "-1");
7897
+ }
7898
+ this.setManagedTimeout(() => {
7899
+ targetElement.focus();
7900
+ targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
7901
+ }, 10);
7902
+ } else if (this.options.debug) {
7903
+ this.log("[Metadata] Element not found:", normalizedSelector || targetSelector);
7904
+ }
7905
+ this.emit("metadata:focus", {
7906
+ time: cue.startTime,
7907
+ target: targetSelector,
7908
+ selector: normalizedSelector,
7909
+ element: targetElement,
7910
+ text
7911
+ });
7912
+ if (normalizedSelector) {
7913
+ this.handleMetadataAlert(normalizedSelector, {
7914
+ element: targetElement,
7915
+ reason: "focus"
7916
+ });
7917
+ }
7918
+ }
7919
+ const hashtags = text.match(/#[\w-]+/g);
7920
+ if (hashtags) {
7921
+ if (this.options.debug) {
7922
+ this.log("[Metadata] Hashtags found:", hashtags);
7923
+ }
7924
+ this.emit("metadata:hashtags", {
7925
+ time: cue.startTime,
7926
+ hashtags,
7927
+ text
7928
+ });
7929
+ this.handleMetadataHashtags(hashtags);
7930
+ }
7931
+ }
6519
7932
  };
6520
7933
  Player.instances = [];
6521
7934