vidply 1.0.8 → 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) {
@@ -5492,12 +5725,21 @@ var VidPly = (() => {
5492
5725
  this.audioDescriptionSourceElement = null;
5493
5726
  this.originalAudioDescriptionSource = null;
5494
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();
5495
5735
  this.container = null;
5496
5736
  this.renderer = null;
5497
5737
  this.controlBar = null;
5498
5738
  this.captionManager = null;
5499
5739
  this.keyboardManager = null;
5500
5740
  this.settingsDialog = null;
5741
+ this.metadataCueChangeHandler = null;
5742
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5501
5743
  this.init();
5502
5744
  }
5503
5745
  async init() {
@@ -5529,6 +5771,7 @@ var VidPly = (() => {
5529
5771
  if (this.options.transcript || this.options.transcriptButton) {
5530
5772
  this.transcriptManager = new TranscriptManager(this);
5531
5773
  }
5774
+ this.setupMetadataHandling();
5532
5775
  if (this.options.keyboard) {
5533
5776
  this.keyboardManager = new KeyboardManager(this);
5534
5777
  }
@@ -5614,6 +5857,8 @@ var VidPly = (() => {
5614
5857
  if (this.element.tagName === "VIDEO") {
5615
5858
  this.createPlayButtonOverlay();
5616
5859
  }
5860
+ this.element.vidply = this;
5861
+ _Player.instances.push(this);
5617
5862
  this.element.style.cursor = "pointer";
5618
5863
  this.element.addEventListener("click", (e) => {
5619
5864
  if (e.target === this.element) {
@@ -5646,7 +5891,7 @@ var VidPly = (() => {
5646
5891
  if (!src) {
5647
5892
  throw new Error("No media source found");
5648
5893
  }
5649
- const sourceElements = this.element.querySelectorAll("source");
5894
+ const sourceElements = this.sourceElements;
5650
5895
  for (const sourceEl of sourceElements) {
5651
5896
  const descSrc = sourceEl.getAttribute("data-desc-src");
5652
5897
  const origSrc = sourceEl.getAttribute("data-orig-src");
@@ -5675,7 +5920,7 @@ var VidPly = (() => {
5675
5920
  }
5676
5921
  }
5677
5922
  }
5678
- const trackElements = this.element.querySelectorAll("track");
5923
+ const trackElements = this.trackElements;
5679
5924
  trackElements.forEach((trackEl) => {
5680
5925
  const trackKind = trackEl.getAttribute("kind");
5681
5926
  const trackDescSrc = trackEl.getAttribute("data-desc-src");
@@ -5709,6 +5954,106 @@ var VidPly = (() => {
5709
5954
  this.log(`Using ${renderer.name} renderer`);
5710
5955
  this.renderer = new renderer(this);
5711
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
+ }
5712
6057
  }
5713
6058
  /**
5714
6059
  * Load new media source (for playlists)
@@ -5724,8 +6069,9 @@ var VidPly = (() => {
5724
6069
  if (this.renderer) {
5725
6070
  this.pause();
5726
6071
  }
5727
- const existingTracks = this.element.querySelectorAll("track");
6072
+ const existingTracks = this.trackElements;
5728
6073
  existingTracks.forEach((track) => track.remove());
6074
+ this.invalidateTrackCache();
5729
6075
  this.element.src = config.src;
5730
6076
  if (config.type) {
5731
6077
  this.element.type = config.type;
@@ -5745,6 +6091,7 @@ var VidPly = (() => {
5745
6091
  }
5746
6092
  this.element.appendChild(track);
5747
6093
  });
6094
+ this.invalidateTrackCache();
5748
6095
  }
5749
6096
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5750
6097
  if (shouldChangeRenderer && this.renderer) {
@@ -5992,7 +6339,7 @@ var VidPly = (() => {
5992
6339
  }
5993
6340
  // Audio Description
5994
6341
  async enableAudioDescription() {
5995
- const hasSourceElementsWithDesc = Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6342
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
5996
6343
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
5997
6344
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
5998
6345
  console.warn("VidPly: No audio description source, source elements, or tracks provided");
@@ -6003,7 +6350,7 @@ var VidPly = (() => {
6003
6350
  let swappedTracksForTranscript = [];
6004
6351
  if (this.audioDescriptionSourceElement) {
6005
6352
  const currentSrc = this.element.currentSrc || this.element.src;
6006
- const sourceElements = Array.from(this.element.querySelectorAll("source"));
6353
+ const sourceElements = this.sourceElements;
6007
6354
  let sourceElementToUpdate = null;
6008
6355
  let descSrc = this.audioDescriptionSrc;
6009
6356
  for (const sourceEl of sourceElements) {
@@ -6100,8 +6447,9 @@ var VidPly = (() => {
6100
6447
  trackInfo.trackElement = newTrackElement;
6101
6448
  });
6102
6449
  this.element.load();
6450
+ this.invalidateTrackCache();
6103
6451
  const setupNewTracks = () => {
6104
- setTimeout(() => {
6452
+ this.setManagedTimeout(() => {
6105
6453
  swappedTracksForTranscript.forEach((trackInfo) => {
6106
6454
  const trackElement = trackInfo.trackElement;
6107
6455
  const newTextTrack = trackElement.track;
@@ -6137,7 +6485,7 @@ var VidPly = (() => {
6137
6485
  const skippedCount = validationResults.length - tracksToSwap.length;
6138
6486
  }
6139
6487
  }
6140
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
6488
+ const allSourceElements = this.sourceElements;
6141
6489
  const sourcesToUpdate = [];
6142
6490
  allSourceElements.forEach((sourceEl) => {
6143
6491
  const descSrcAttr = sourceEl.getAttribute("data-desc-src");
@@ -6315,7 +6663,7 @@ var VidPly = (() => {
6315
6663
  }, 100);
6316
6664
  }
6317
6665
  }
6318
- const fallbackSourceElements = Array.from(this.element.querySelectorAll("source"));
6666
+ const fallbackSourceElements = this.sourceElements;
6319
6667
  const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6320
6668
  if (hasSourceElementsWithDesc2) {
6321
6669
  const fallbackSourcesToUpdate = [];
@@ -6363,6 +6711,7 @@ var VidPly = (() => {
6363
6711
  this.element.appendChild(newSource);
6364
6712
  });
6365
6713
  this.element.load();
6714
+ this.invalidateTrackCache();
6366
6715
  } else {
6367
6716
  this.element.src = this.audioDescriptionSrc;
6368
6717
  }
@@ -6377,7 +6726,7 @@ var VidPly = (() => {
6377
6726
  if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6378
6727
  if (this.element.readyState >= 1) {
6379
6728
  this.element.currentTime = 1e-3;
6380
- setTimeout(() => {
6729
+ this.setManagedTimeout(() => {
6381
6730
  this.element.currentTime = 0;
6382
6731
  }, 10);
6383
6732
  }
@@ -6417,7 +6766,8 @@ var VidPly = (() => {
6417
6766
  const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6418
6767
  if (swappedTracks.length > 0) {
6419
6768
  const onMetadataLoaded = () => {
6420
- const allTextTracks = Array.from(this.element.textTracks);
6769
+ this.invalidateTrackCache();
6770
+ const allTextTracks = this.textTracks;
6421
6771
  const freshTracks = swappedTracks.map((trackInfo) => {
6422
6772
  const trackEl = trackInfo.trackElement;
6423
6773
  const expectedSrc = trackEl.getAttribute("src");
@@ -6427,9 +6777,7 @@ var VidPly = (() => {
6427
6777
  if (!foundTrack) {
6428
6778
  foundTrack = allTextTracks.find((track) => {
6429
6779
  if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6430
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6431
- (el) => el.track === track
6432
- );
6780
+ const trackElementForTrack = this.findTrackElement(track);
6433
6781
  if (trackElementForTrack) {
6434
6782
  const actualSrc = trackElementForTrack.getAttribute("src");
6435
6783
  if (actualSrc === expectedSrc) {
@@ -6441,9 +6789,7 @@ var VidPly = (() => {
6441
6789
  });
6442
6790
  }
6443
6791
  if (foundTrack) {
6444
- const trackElement = Array.from(this.element.querySelectorAll("track")).find(
6445
- (el) => el.track === foundTrack
6446
- );
6792
+ const trackElement = this.findTrackElement(foundTrack);
6447
6793
  if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6448
6794
  return null;
6449
6795
  }
@@ -6451,7 +6797,7 @@ var VidPly = (() => {
6451
6797
  return foundTrack;
6452
6798
  }).filter(Boolean);
6453
6799
  if (freshTracks.length === 0) {
6454
- setTimeout(() => {
6800
+ this.setManagedTimeout(() => {
6455
6801
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6456
6802
  this.transcriptManager.loadTranscriptData();
6457
6803
  }
@@ -6467,14 +6813,13 @@ var VidPly = (() => {
6467
6813
  const checkLoaded = () => {
6468
6814
  loadedCount++;
6469
6815
  if (loadedCount >= freshTracks.length) {
6470
- setTimeout(() => {
6816
+ this.setManagedTimeout(() => {
6471
6817
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6472
- const allTextTracks2 = Array.from(this.element.textTracks);
6818
+ this.invalidateTrackCache();
6819
+ const allTextTracks2 = this.textTracks;
6473
6820
  const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6474
6821
  const hasCorrectTracks = freshTracks.some((track) => {
6475
- const trackEl = Array.from(this.element.querySelectorAll("track")).find(
6476
- (el) => el.track === track
6477
- );
6822
+ const trackEl = this.findTrackElement(track);
6478
6823
  return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6479
6824
  });
6480
6825
  if (hasCorrectTracks || freshTracks.length > 0) {
@@ -6488,9 +6833,7 @@ var VidPly = (() => {
6488
6833
  if (track.mode === "disabled") {
6489
6834
  track.mode = "hidden";
6490
6835
  }
6491
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6492
- (el) => el.track === track
6493
- );
6836
+ const trackElementForTrack = this.findTrackElement(track);
6494
6837
  const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6495
6838
  const expectedTrackInfo = swappedTracks.find((t) => {
6496
6839
  const tEl = t.trackElement;
@@ -6508,10 +6851,10 @@ var VidPly = (() => {
6508
6851
  track.mode = "hidden";
6509
6852
  }
6510
6853
  const onTrackLoad = () => {
6511
- setTimeout(checkLoaded, 300);
6854
+ this.setManagedTimeout(checkLoaded, 300);
6512
6855
  };
6513
6856
  if (track.readyState >= 2) {
6514
- setTimeout(() => {
6857
+ this.setManagedTimeout(() => {
6515
6858
  if (track.cues && track.cues.length > 0) {
6516
6859
  checkLoaded();
6517
6860
  } else {
@@ -6528,12 +6871,12 @@ var VidPly = (() => {
6528
6871
  });
6529
6872
  };
6530
6873
  const waitForTracks = () => {
6531
- setTimeout(() => {
6874
+ this.setManagedTimeout(() => {
6532
6875
  if (this.element.readyState >= 1) {
6533
6876
  onMetadataLoaded();
6534
6877
  } else {
6535
6878
  this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6536
- setTimeout(onMetadataLoaded, 2e3);
6879
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6537
6880
  }
6538
6881
  }, 500);
6539
6882
  };
@@ -6567,7 +6910,7 @@ var VidPly = (() => {
6567
6910
  }
6568
6911
  });
6569
6912
  }
6570
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
6913
+ const allSourceElements = this.sourceElements;
6571
6914
  const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6572
6915
  if (hasSourceElementsToSwap) {
6573
6916
  const sourcesToRestore = [];
@@ -6630,7 +6973,7 @@ var VidPly = (() => {
6630
6973
  this.play();
6631
6974
  }
6632
6975
  if (this.transcriptManager && this.transcriptManager.isVisible) {
6633
- setTimeout(() => {
6976
+ this.setManagedTimeout(() => {
6634
6977
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6635
6978
  this.transcriptManager.loadTranscriptData();
6636
6979
  }
@@ -6640,16 +6983,37 @@ var VidPly = (() => {
6640
6983
  this.emit("audiodescriptiondisabled");
6641
6984
  }
6642
6985
  async toggleAudioDescription() {
6643
- const textTracks = Array.from(this.element.textTracks || []);
6644
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
6645
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6986
+ const descriptionTrack = this.findTextTrack("descriptions");
6987
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6646
6988
  if (descriptionTrack && hasAudioDescriptionSrc) {
6647
6989
  if (this.state.audioDescriptionEnabled) {
6648
6990
  descriptionTrack.mode = "hidden";
6649
6991
  await this.disableAudioDescription();
6650
6992
  } else {
6651
6993
  await this.enableAudioDescription();
6652
- descriptionTrack.mode = "showing";
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
+ }
6653
7017
  }
6654
7018
  } else if (descriptionTrack) {
6655
7019
  if (descriptionTrack.mode === "showing") {
@@ -7062,9 +7426,25 @@ var VidPly = (() => {
7062
7426
  }
7063
7427
  }
7064
7428
  // Logging
7065
- log(message, type = "log") {
7066
- if (this.options.debug) {
7067
- 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);
7068
7448
  }
7069
7449
  }
7070
7450
  // Setup responsive handlers
@@ -7124,7 +7504,7 @@ var VidPly = (() => {
7124
7504
  this.controlBar.updateFullscreenButton();
7125
7505
  }
7126
7506
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
7127
- setTimeout(() => {
7507
+ this.setManagedTimeout(() => {
7128
7508
  requestAnimationFrame(() => {
7129
7509
  this.storage.saveSignLanguagePreferences({ size: null });
7130
7510
  this.signLanguageDesiredPosition = "bottom-right";
@@ -7187,12 +7567,368 @@ var VidPly = (() => {
7187
7567
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
7188
7568
  this.fullscreenChangeHandler = null;
7189
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
+ }
7190
7588
  if (this.container && this.container.parentNode) {
7191
7589
  this.container.parentNode.insertBefore(this.element, this.container);
7192
7590
  this.container.parentNode.removeChild(this.container);
7193
7591
  }
7194
7592
  this.removeAllListeners();
7195
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
+ }
7196
7932
  };
7197
7933
  Player.instances = [];
7198
7934