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.
@@ -500,7 +500,8 @@ var translations = {
500
500
  resizeWindow: "Resize Window",
501
501
  styleTranscript: "Open transcript style settings",
502
502
  closeMenu: "Close Menu",
503
- styleTitle: "Transcript Style"
503
+ styleTitle: "Transcript Style",
504
+ autoscroll: "Autoscroll"
504
505
  },
505
506
  settings: {
506
507
  title: "Settings",
@@ -621,7 +622,8 @@ var translations = {
621
622
  resizeWindow: "Fenster vergr\xF6\xDFern/verkleinern",
622
623
  styleTranscript: "Transkript-Stileinstellungen \xF6ffnen",
623
624
  closeMenu: "Men\xFC schlie\xDFen",
624
- styleTitle: "Transkript-Stil"
625
+ styleTitle: "Transkript-Stil",
626
+ autoscroll: "Automatisches Scrollen"
625
627
  },
626
628
  settings: {
627
629
  title: "Einstellungen",
@@ -742,7 +744,8 @@ var translations = {
742
744
  resizeWindow: "Cambiar tama\xF1o de ventana",
743
745
  styleTranscript: "Abrir configuraci\xF3n de estilo de transcripci\xF3n",
744
746
  closeMenu: "Cerrar men\xFA",
745
- styleTitle: "Estilo de Transcripci\xF3n"
747
+ styleTitle: "Estilo de Transcripci\xF3n",
748
+ autoscroll: "Desplazamiento autom\xE1tico"
746
749
  },
747
750
  settings: {
748
751
  title: "Configuraci\xF3n",
@@ -863,7 +866,8 @@ var translations = {
863
866
  resizeWindow: "Redimensionner la fen\xEAtre",
864
867
  styleTranscript: "Ouvrir les param\xE8tres de style de transcription",
865
868
  closeMenu: "Fermer le menu",
866
- styleTitle: "Style de Transcription"
869
+ styleTitle: "Style de Transcription",
870
+ autoscroll: "D\xE9filement automatique"
867
871
  },
868
872
  settings: {
869
873
  title: "Param\xE8tres",
@@ -984,7 +988,8 @@ var translations = {
984
988
  resizeWindow: "\u30A6\u30A3\u30F3\u30C9\u30A6\u306E\u30B5\u30A4\u30BA\u5909\u66F4",
985
989
  styleTranscript: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB\u8A2D\u5B9A\u3092\u958B\u304F",
986
990
  closeMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u9589\u3058\u308B",
987
- styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB"
991
+ styleTitle: "\u6587\u5B57\u8D77\u3053\u3057\u30B9\u30BF\u30A4\u30EB",
992
+ autoscroll: "\u81EA\u52D5\u30B9\u30AF\u30ED\u30FC\u30EB"
988
993
  },
989
994
  settings: {
990
995
  title: "\u8A2D\u5B9A",
@@ -1163,8 +1168,8 @@ var iconPaths = {
1163
1168
  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"/>`,
1164
1169
  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"/>`,
1165
1170
  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"/>`,
1166
- 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>`,
1167
- 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>`,
1171
+ 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>`,
1172
+ 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>`,
1168
1173
  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>`,
1169
1174
  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>`,
1170
1175
  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"/>`,
@@ -3321,7 +3326,12 @@ var TranscriptManager = class {
3321
3326
  this.styleDialog = null;
3322
3327
  this.styleDialogVisible = false;
3323
3328
  this.styleDialogJustOpened = false;
3329
+ this.languageSelector = null;
3330
+ this.currentTranscriptLanguage = null;
3331
+ this.availableTranscriptLanguages = [];
3332
+ this.languageSelectorHandler = null;
3324
3333
  const savedPreferences = this.storage.getTranscriptPreferences();
3334
+ this.autoscrollEnabled = (savedPreferences == null ? void 0 : savedPreferences.autoscroll) !== void 0 ? savedPreferences.autoscroll : true;
3325
3335
  this.transcriptStyle = {
3326
3336
  fontSize: (savedPreferences == null ? void 0 : savedPreferences.fontSize) || this.player.options.transcriptFontSize || "100%",
3327
3337
  fontFamily: (savedPreferences == null ? void 0 : savedPreferences.fontFamily) || this.player.options.transcriptFontFamily || "sans-serif",
@@ -3344,13 +3354,15 @@ var TranscriptManager = class {
3344
3354
  documentClick: null,
3345
3355
  styleDialogKeydown: null
3346
3356
  };
3357
+ this.timeouts = /* @__PURE__ */ new Set();
3347
3358
  this.init();
3348
3359
  }
3349
3360
  init() {
3361
+ this.setupMetadataHandlingOnLoad();
3350
3362
  this.player.on("timeupdate", this.handlers.timeupdate);
3351
3363
  this.player.on("fullscreenchange", () => {
3352
3364
  if (this.isVisible) {
3353
- setTimeout(() => this.positionTranscript(), 100);
3365
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
3354
3366
  }
3355
3367
  });
3356
3368
  }
@@ -3371,7 +3383,7 @@ var TranscriptManager = class {
3371
3383
  if (this.transcriptWindow) {
3372
3384
  this.transcriptWindow.style.display = "flex";
3373
3385
  this.isVisible = true;
3374
- setTimeout(() => {
3386
+ this.setManagedTimeout(() => {
3375
3387
  if (this.settingsButton) {
3376
3388
  this.settingsButton.focus();
3377
3389
  }
@@ -3382,8 +3394,8 @@ var TranscriptManager = class {
3382
3394
  this.loadTranscriptData();
3383
3395
  if (this.transcriptWindow) {
3384
3396
  this.transcriptWindow.style.display = "flex";
3385
- setTimeout(() => this.positionTranscript(), 0);
3386
- setTimeout(() => {
3397
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
3398
+ this.setManagedTimeout(() => {
3387
3399
  if (this.settingsButton) {
3388
3400
  this.settingsButton.focus();
3389
3401
  }
@@ -3460,8 +3472,41 @@ var TranscriptManager = class {
3460
3472
  const title = DOMUtils.createElement("h3", {
3461
3473
  textContent: i18n.t("transcript.title")
3462
3474
  });
3475
+ const autoscrollLabel = DOMUtils.createElement("label", {
3476
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
3477
+ attributes: {
3478
+ "title": i18n.t("transcript.autoscroll")
3479
+ }
3480
+ });
3481
+ this.autoscrollCheckbox = DOMUtils.createElement("input", {
3482
+ attributes: {
3483
+ "type": "checkbox",
3484
+ "checked": this.autoscrollEnabled,
3485
+ "aria-label": i18n.t("transcript.autoscroll")
3486
+ }
3487
+ });
3488
+ const autoscrollText = DOMUtils.createElement("span", {
3489
+ textContent: i18n.t("transcript.autoscroll"),
3490
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
3491
+ });
3492
+ autoscrollLabel.appendChild(this.autoscrollCheckbox);
3493
+ autoscrollLabel.appendChild(autoscrollText);
3494
+ this.autoscrollCheckbox.addEventListener("change", (e) => {
3495
+ this.autoscrollEnabled = e.target.checked;
3496
+ this.saveAutoscrollPreference();
3497
+ });
3463
3498
  this.headerLeft.appendChild(this.settingsButton);
3464
3499
  this.headerLeft.appendChild(title);
3500
+ this.headerLeft.appendChild(autoscrollLabel);
3501
+ this.languageSelector = DOMUtils.createElement("select", {
3502
+ className: `${this.player.options.classPrefix}-transcript-language-select`,
3503
+ attributes: {
3504
+ "aria-label": i18n.t("settings.language") || "Language",
3505
+ "style": "display: none;"
3506
+ // Hidden until we detect multiple languages
3507
+ }
3508
+ });
3509
+ this.headerLeft.appendChild(this.languageSelector);
3465
3510
  const closeButton = DOMUtils.createElement("button", {
3466
3511
  className: `${this.player.options.classPrefix}-transcript-close`,
3467
3512
  attributes: {
@@ -3504,8 +3549,10 @@ var TranscriptManager = class {
3504
3549
  this.documentClickHandlerAdded = false;
3505
3550
  let resizeTimeout;
3506
3551
  this.handlers.resize = () => {
3507
- clearTimeout(resizeTimeout);
3508
- resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
3552
+ if (resizeTimeout) {
3553
+ this.clearManagedTimeout(resizeTimeout);
3554
+ }
3555
+ resizeTimeout = this.setManagedTimeout(() => this.positionTranscript(), 100);
3509
3556
  };
3510
3557
  window.addEventListener("resize", this.handlers.resize);
3511
3558
  }
@@ -3575,17 +3622,94 @@ var TranscriptManager = class {
3575
3622
  }
3576
3623
  }
3577
3624
  }
3625
+ /**
3626
+ * Get available transcript languages from tracks
3627
+ */
3628
+ getAvailableTranscriptLanguages() {
3629
+ const textTracks = this.player.textTracks;
3630
+ const languages = /* @__PURE__ */ new Map();
3631
+ textTracks.forEach((track) => {
3632
+ if ((track.kind === "captions" || track.kind === "subtitles") && track.language) {
3633
+ if (!languages.has(track.language)) {
3634
+ languages.set(track.language, {
3635
+ language: track.language,
3636
+ label: track.label || track.language,
3637
+ track
3638
+ });
3639
+ }
3640
+ }
3641
+ });
3642
+ return Array.from(languages.values());
3643
+ }
3644
+ /**
3645
+ * Update language selector dropdown
3646
+ */
3647
+ updateLanguageSelector() {
3648
+ if (!this.languageSelector) return;
3649
+ this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
3650
+ this.languageSelector.innerHTML = "";
3651
+ if (this.availableTranscriptLanguages.length < 2) {
3652
+ this.languageSelector.style.display = "none";
3653
+ return;
3654
+ }
3655
+ this.languageSelector.style.display = "block";
3656
+ this.availableTranscriptLanguages.forEach((langInfo, index) => {
3657
+ const option = DOMUtils.createElement("option", {
3658
+ textContent: langInfo.label,
3659
+ attributes: {
3660
+ "value": langInfo.language
3661
+ }
3662
+ });
3663
+ this.languageSelector.appendChild(option);
3664
+ });
3665
+ if (this.currentTranscriptLanguage) {
3666
+ this.languageSelector.value = this.currentTranscriptLanguage;
3667
+ } else if (this.availableTranscriptLanguages.length > 0) {
3668
+ const activeTrack = this.player.textTracks.find(
3669
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.mode === "showing"
3670
+ );
3671
+ this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
3672
+ this.languageSelector.value = this.currentTranscriptLanguage;
3673
+ }
3674
+ if (this.languageSelectorHandler) {
3675
+ this.languageSelector.removeEventListener("change", this.languageSelectorHandler);
3676
+ }
3677
+ this.languageSelectorHandler = (e) => {
3678
+ this.currentTranscriptLanguage = e.target.value;
3679
+ this.loadTranscriptData();
3680
+ };
3681
+ this.languageSelector.addEventListener("change", this.languageSelectorHandler);
3682
+ }
3578
3683
  /**
3579
3684
  * Load transcript data from caption/subtitle tracks
3580
3685
  */
3581
3686
  loadTranscriptData() {
3582
3687
  this.transcriptEntries = [];
3583
3688
  this.transcriptContent.innerHTML = "";
3584
- const textTracks = Array.from(this.player.element.textTracks);
3585
- const captionTrack = textTracks.find(
3586
- (track) => track.kind === "captions" || track.kind === "subtitles"
3587
- );
3588
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3689
+ const textTracks = this.player.textTracks;
3690
+ let captionTrack = null;
3691
+ if (this.currentTranscriptLanguage) {
3692
+ captionTrack = textTracks.find(
3693
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.language === this.currentTranscriptLanguage
3694
+ );
3695
+ }
3696
+ if (!captionTrack) {
3697
+ captionTrack = textTracks.find(
3698
+ (track) => track.kind === "captions" || track.kind === "subtitles"
3699
+ );
3700
+ if (captionTrack) {
3701
+ this.currentTranscriptLanguage = captionTrack.language;
3702
+ }
3703
+ }
3704
+ let descriptionTrack = null;
3705
+ if (this.currentTranscriptLanguage) {
3706
+ descriptionTrack = textTracks.find(
3707
+ (track) => track.kind === "descriptions" && track.language === this.currentTranscriptLanguage
3708
+ );
3709
+ }
3710
+ if (!descriptionTrack) {
3711
+ descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3712
+ }
3589
3713
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3590
3714
  if (!captionTrack && !descriptionTrack && !metadataTrack) {
3591
3715
  this.showNoTranscriptMessage();
@@ -3614,7 +3738,7 @@ var TranscriptManager = class {
3614
3738
  tracksToLoad.forEach((track) => {
3615
3739
  track.addEventListener("load", onLoad, { once: true });
3616
3740
  });
3617
- setTimeout(() => {
3741
+ this.setManagedTimeout(() => {
3618
3742
  this.loadTranscriptData();
3619
3743
  }, 500);
3620
3744
  return;
@@ -3647,24 +3771,61 @@ var TranscriptManager = class {
3647
3771
  this.transcriptContent.appendChild(entry);
3648
3772
  });
3649
3773
  this.applyTranscriptStyles();
3774
+ this.updateLanguageSelector();
3775
+ }
3776
+ /**
3777
+ * Setup metadata handling on player load
3778
+ * This runs independently of transcript loading
3779
+ */
3780
+ setupMetadataHandlingOnLoad() {
3781
+ const setupMetadata = () => {
3782
+ const textTracks = this.player.textTracks;
3783
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3784
+ if (metadataTrack) {
3785
+ if (metadataTrack.mode === "disabled") {
3786
+ metadataTrack.mode = "hidden";
3787
+ }
3788
+ if (this.metadataCueChangeHandler) {
3789
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
3790
+ }
3791
+ this.metadataCueChangeHandler = () => {
3792
+ const activeCues = Array.from(metadataTrack.activeCues || []);
3793
+ if (activeCues.length > 0) {
3794
+ if (this.player.options.debug) {
3795
+ console.log("[VidPly Metadata] Active cues:", activeCues.map((c) => ({
3796
+ start: c.startTime,
3797
+ end: c.endTime,
3798
+ text: c.text
3799
+ })));
3800
+ }
3801
+ }
3802
+ activeCues.forEach((cue) => {
3803
+ this.handleMetadataCue(cue);
3804
+ });
3805
+ };
3806
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
3807
+ if (this.player.options.debug) {
3808
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
3809
+ console.log("[VidPly Metadata] Track enabled,", cueCount, "cues available");
3810
+ }
3811
+ } else if (this.player.options.debug) {
3812
+ console.warn("[VidPly Metadata] No metadata track found");
3813
+ }
3814
+ };
3815
+ setupMetadata();
3816
+ this.player.on("loadedmetadata", setupMetadata);
3650
3817
  }
3651
3818
  /**
3652
3819
  * Setup metadata handling
3653
3820
  * Metadata cues are not displayed but can be used programmatically
3821
+ * This is called when transcript data is loaded (for storing cues)
3654
3822
  */
3655
3823
  setupMetadataHandling() {
3656
3824
  if (!this.metadataCues || this.metadataCues.length === 0) {
3657
3825
  return;
3658
3826
  }
3659
- const textTracks = Array.from(this.player.element.textTracks);
3660
- const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3661
- if (metadataTrack) {
3662
- metadataTrack.addEventListener("cuechange", () => {
3663
- const activeCues = Array.from(metadataTrack.activeCues || []);
3664
- activeCues.forEach((cue) => {
3665
- this.handleMetadataCue(cue);
3666
- });
3667
- });
3827
+ if (this.player.options.debug) {
3828
+ console.log("[VidPly Metadata]", this.metadataCues.length, "cues stored from transcript load");
3668
3829
  }
3669
3830
  }
3670
3831
  /**
@@ -3673,6 +3834,12 @@ var TranscriptManager = class {
3673
3834
  */
3674
3835
  handleMetadataCue(cue) {
3675
3836
  const text = cue.text.trim();
3837
+ if (this.player.options.debug) {
3838
+ console.log("[VidPly Metadata] Processing cue:", {
3839
+ time: cue.startTime,
3840
+ text
3841
+ });
3842
+ }
3676
3843
  this.player.emit("metadata", {
3677
3844
  time: cue.startTime,
3678
3845
  endTime: cue.endTime,
@@ -3680,18 +3847,40 @@ var TranscriptManager = class {
3680
3847
  cue
3681
3848
  });
3682
3849
  if (text.includes("PAUSE")) {
3850
+ if (!this.player.state.paused) {
3851
+ if (this.player.options.debug) {
3852
+ console.log("[VidPly Metadata] Pausing video at", cue.startTime);
3853
+ }
3854
+ this.player.pause();
3855
+ }
3683
3856
  this.player.emit("metadata:pause", { time: cue.startTime, text });
3684
3857
  }
3685
3858
  const focusMatch = text.match(/FOCUS:([\w#-]+)/);
3686
3859
  if (focusMatch) {
3860
+ const targetSelector = focusMatch[1];
3861
+ const targetElement = document.querySelector(targetSelector);
3862
+ if (targetElement) {
3863
+ if (this.player.options.debug) {
3864
+ console.log("[VidPly Metadata] Focusing element:", targetSelector);
3865
+ }
3866
+ this.setManagedTimeout(() => {
3867
+ targetElement.focus();
3868
+ }, 10);
3869
+ } else if (this.player.options.debug) {
3870
+ console.warn("[VidPly Metadata] Element not found:", targetSelector);
3871
+ }
3687
3872
  this.player.emit("metadata:focus", {
3688
3873
  time: cue.startTime,
3689
- target: focusMatch[1],
3874
+ target: targetSelector,
3875
+ element: targetElement,
3690
3876
  text
3691
3877
  });
3692
3878
  }
3693
3879
  const hashtags = text.match(/#[\w-]+/g);
3694
3880
  if (hashtags) {
3881
+ if (this.player.options.debug) {
3882
+ console.log("[VidPly Metadata] Hashtags found:", hashtags);
3883
+ }
3695
3884
  this.player.emit("metadata:hashtags", {
3696
3885
  time: cue.startTime,
3697
3886
  hashtags,
@@ -3785,7 +3974,7 @@ var TranscriptManager = class {
3785
3974
  * Scroll transcript window to show active entry
3786
3975
  */
3787
3976
  scrollToEntry(entryElement) {
3788
- if (!this.transcriptContent) return;
3977
+ if (!this.transcriptContent || !this.autoscrollEnabled) return;
3789
3978
  const contentRect = this.transcriptContent.getBoundingClientRect();
3790
3979
  const entryRect = entryElement.getBoundingClientRect();
3791
3980
  if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
@@ -3796,6 +3985,14 @@ var TranscriptManager = class {
3796
3985
  });
3797
3986
  }
3798
3987
  }
3988
+ /**
3989
+ * Save autoscroll preference to localStorage
3990
+ */
3991
+ saveAutoscrollPreference() {
3992
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
3993
+ savedPreferences.autoscroll = this.autoscrollEnabled;
3994
+ this.storage.saveTranscriptPreferences(savedPreferences);
3995
+ }
3799
3996
  /**
3800
3997
  * Setup drag and drop functionality
3801
3998
  */
@@ -3808,6 +4005,9 @@ var TranscriptManager = class {
3808
4005
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3809
4006
  return;
3810
4007
  }
4008
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
4009
+ return;
4010
+ }
3811
4011
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3812
4012
  return;
3813
4013
  }
@@ -3834,6 +4034,9 @@ var TranscriptManager = class {
3834
4034
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
3835
4035
  return;
3836
4036
  }
4037
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
4038
+ return;
4039
+ }
3837
4040
  if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
3838
4041
  return;
3839
4042
  }
@@ -4588,6 +4791,30 @@ var TranscriptManager = class {
4588
4791
  entry.style.fontFamily = this.transcriptStyle.fontFamily;
4589
4792
  });
4590
4793
  }
4794
+ /**
4795
+ * Set a managed timeout that will be cleaned up on destroy
4796
+ * @param {Function} callback - Callback function
4797
+ * @param {number} delay - Delay in milliseconds
4798
+ * @returns {number} Timeout ID
4799
+ */
4800
+ setManagedTimeout(callback, delay) {
4801
+ const timeoutId = setTimeout(() => {
4802
+ this.timeouts.delete(timeoutId);
4803
+ callback();
4804
+ }, delay);
4805
+ this.timeouts.add(timeoutId);
4806
+ return timeoutId;
4807
+ }
4808
+ /**
4809
+ * Clear a managed timeout
4810
+ * @param {number} timeoutId - Timeout ID to clear
4811
+ */
4812
+ clearManagedTimeout(timeoutId) {
4813
+ if (timeoutId) {
4814
+ clearTimeout(timeoutId);
4815
+ this.timeouts.delete(timeoutId);
4816
+ }
4817
+ }
4591
4818
  /**
4592
4819
  * Cleanup
4593
4820
  */
@@ -4641,6 +4868,8 @@ var TranscriptManager = class {
4641
4868
  if (this.handlers.resize) {
4642
4869
  window.removeEventListener("resize", this.handlers.resize);
4643
4870
  }
4871
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
4872
+ this.timeouts.clear();
4644
4873
  this.handlers = null;
4645
4874
  if (this.transcriptWindow && this.transcriptWindow.parentNode) {
4646
4875
  this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
@@ -5312,7 +5541,7 @@ var HLSRenderer = class {
5312
5541
  };
5313
5542
 
5314
5543
  // src/core/Player.js
5315
- var Player = class extends EventEmitter {
5544
+ var Player = class _Player extends EventEmitter {
5316
5545
  constructor(element, options = {}) {
5317
5546
  super();
5318
5547
  this.element = typeof element === "string" ? document.querySelector(element) : element;
@@ -5420,6 +5649,8 @@ var Player = class extends EventEmitter {
5420
5649
  screenReaderAnnouncements: true,
5421
5650
  highContrast: false,
5422
5651
  focusHighlight: true,
5652
+ metadataAlerts: {},
5653
+ metadataHashtags: {},
5423
5654
  // Languages
5424
5655
  language: "en",
5425
5656
  languages: ["en"],
@@ -5438,6 +5669,8 @@ var Player = class extends EventEmitter {
5438
5669
  onError: null,
5439
5670
  ...options
5440
5671
  };
5672
+ this.options.metadataAlerts = this.options.metadataAlerts || {};
5673
+ this.options.metadataHashtags = this.options.metadataHashtags || {};
5441
5674
  this.storage = new StorageManager("vidply");
5442
5675
  const savedPrefs = this.storage.getPlayerPreferences();
5443
5676
  if (savedPrefs) {
@@ -5472,12 +5705,21 @@ var Player = class extends EventEmitter {
5472
5705
  this.audioDescriptionSourceElement = null;
5473
5706
  this.originalAudioDescriptionSource = null;
5474
5707
  this.audioDescriptionCaptionTracks = [];
5708
+ this._textTracksCache = null;
5709
+ this._textTracksDirty = true;
5710
+ this._sourceElementsCache = null;
5711
+ this._sourceElementsDirty = true;
5712
+ this._trackElementsCache = null;
5713
+ this._trackElementsDirty = true;
5714
+ this.timeouts = /* @__PURE__ */ new Set();
5475
5715
  this.container = null;
5476
5716
  this.renderer = null;
5477
5717
  this.controlBar = null;
5478
5718
  this.captionManager = null;
5479
5719
  this.keyboardManager = null;
5480
5720
  this.settingsDialog = null;
5721
+ this.metadataCueChangeHandler = null;
5722
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5481
5723
  this.init();
5482
5724
  }
5483
5725
  async init() {
@@ -5509,6 +5751,7 @@ var Player = class extends EventEmitter {
5509
5751
  if (this.options.transcript || this.options.transcriptButton) {
5510
5752
  this.transcriptManager = new TranscriptManager(this);
5511
5753
  }
5754
+ this.setupMetadataHandling();
5512
5755
  if (this.options.keyboard) {
5513
5756
  this.keyboardManager = new KeyboardManager(this);
5514
5757
  }
@@ -5594,6 +5837,8 @@ var Player = class extends EventEmitter {
5594
5837
  if (this.element.tagName === "VIDEO") {
5595
5838
  this.createPlayButtonOverlay();
5596
5839
  }
5840
+ this.element.vidply = this;
5841
+ _Player.instances.push(this);
5597
5842
  this.element.style.cursor = "pointer";
5598
5843
  this.element.addEventListener("click", (e) => {
5599
5844
  if (e.target === this.element) {
@@ -5626,7 +5871,7 @@ var Player = class extends EventEmitter {
5626
5871
  if (!src) {
5627
5872
  throw new Error("No media source found");
5628
5873
  }
5629
- const sourceElements = this.element.querySelectorAll("source");
5874
+ const sourceElements = this.sourceElements;
5630
5875
  for (const sourceEl of sourceElements) {
5631
5876
  const descSrc = sourceEl.getAttribute("data-desc-src");
5632
5877
  const origSrc = sourceEl.getAttribute("data-orig-src");
@@ -5655,7 +5900,7 @@ var Player = class extends EventEmitter {
5655
5900
  }
5656
5901
  }
5657
5902
  }
5658
- const trackElements = this.element.querySelectorAll("track");
5903
+ const trackElements = this.trackElements;
5659
5904
  trackElements.forEach((trackEl) => {
5660
5905
  const trackKind = trackEl.getAttribute("kind");
5661
5906
  const trackDescSrc = trackEl.getAttribute("data-desc-src");
@@ -5689,6 +5934,106 @@ var Player = class extends EventEmitter {
5689
5934
  this.log(`Using ${renderer.name} renderer`);
5690
5935
  this.renderer = new renderer(this);
5691
5936
  await this.renderer.init();
5937
+ this.invalidateTrackCache();
5938
+ }
5939
+ /**
5940
+ * Get cached text tracks array
5941
+ * @returns {Array} Array of text tracks
5942
+ */
5943
+ get textTracks() {
5944
+ if (!this._textTracksCache || this._textTracksDirty) {
5945
+ this._textTracksCache = Array.from(this.element.textTracks || []);
5946
+ this._textTracksDirty = false;
5947
+ }
5948
+ return this._textTracksCache;
5949
+ }
5950
+ /**
5951
+ * Get cached source elements array
5952
+ * @returns {Array} Array of source elements
5953
+ */
5954
+ get sourceElements() {
5955
+ if (!this._sourceElementsCache || this._sourceElementsDirty) {
5956
+ this._sourceElementsCache = Array.from(this.element.querySelectorAll("source"));
5957
+ this._sourceElementsDirty = false;
5958
+ }
5959
+ return this._sourceElementsCache;
5960
+ }
5961
+ /**
5962
+ * Get cached track elements array
5963
+ * @returns {Array} Array of track elements
5964
+ */
5965
+ get trackElements() {
5966
+ if (!this._trackElementsCache || this._trackElementsDirty) {
5967
+ this._trackElementsCache = Array.from(this.element.querySelectorAll("track"));
5968
+ this._trackElementsDirty = false;
5969
+ }
5970
+ return this._trackElementsCache;
5971
+ }
5972
+ /**
5973
+ * Invalidate DOM query cache (call when tracks/sources change)
5974
+ */
5975
+ invalidateTrackCache() {
5976
+ this._textTracksDirty = true;
5977
+ this._trackElementsDirty = true;
5978
+ this._sourceElementsDirty = true;
5979
+ }
5980
+ /**
5981
+ * Find a text track by kind and optionally language
5982
+ * @param {string} kind - Track kind (captions, subtitles, descriptions, chapters, metadata)
5983
+ * @param {string} [language] - Optional language code
5984
+ * @returns {TextTrack|null} Found track or null
5985
+ */
5986
+ findTextTrack(kind, language = null) {
5987
+ const tracks = this.textTracks;
5988
+ if (language) {
5989
+ return tracks.find((t) => t.kind === kind && t.language === language);
5990
+ }
5991
+ return tracks.find((t) => t.kind === kind);
5992
+ }
5993
+ /**
5994
+ * Find a source element by attribute
5995
+ * @param {string} attribute - Attribute name (e.g., 'data-desc-src')
5996
+ * @param {string} [value] - Optional attribute value
5997
+ * @returns {Element|null} Found source element or null
5998
+ */
5999
+ findSourceElement(attribute, value = null) {
6000
+ const sources = this.sourceElements;
6001
+ if (value) {
6002
+ return sources.find((el) => el.getAttribute(attribute) === value);
6003
+ }
6004
+ return sources.find((el) => el.hasAttribute(attribute));
6005
+ }
6006
+ /**
6007
+ * Find a track element by its associated TextTrack
6008
+ * @param {TextTrack} track - The TextTrack object
6009
+ * @returns {Element|null} Found track element or null
6010
+ */
6011
+ findTrackElement(track) {
6012
+ return this.trackElements.find((el) => el.track === track);
6013
+ }
6014
+ /**
6015
+ * Set a managed timeout that will be cleaned up on destroy
6016
+ * @param {Function} callback - Callback function
6017
+ * @param {number} delay - Delay in milliseconds
6018
+ * @returns {number} Timeout ID
6019
+ */
6020
+ setManagedTimeout(callback, delay) {
6021
+ const timeoutId = setTimeout(() => {
6022
+ this.timeouts.delete(timeoutId);
6023
+ callback();
6024
+ }, delay);
6025
+ this.timeouts.add(timeoutId);
6026
+ return timeoutId;
6027
+ }
6028
+ /**
6029
+ * Clear a managed timeout
6030
+ * @param {number} timeoutId - Timeout ID to clear
6031
+ */
6032
+ clearManagedTimeout(timeoutId) {
6033
+ if (timeoutId) {
6034
+ clearTimeout(timeoutId);
6035
+ this.timeouts.delete(timeoutId);
6036
+ }
5692
6037
  }
5693
6038
  /**
5694
6039
  * Load new media source (for playlists)
@@ -5704,8 +6049,9 @@ var Player = class extends EventEmitter {
5704
6049
  if (this.renderer) {
5705
6050
  this.pause();
5706
6051
  }
5707
- const existingTracks = this.element.querySelectorAll("track");
6052
+ const existingTracks = this.trackElements;
5708
6053
  existingTracks.forEach((track) => track.remove());
6054
+ this.invalidateTrackCache();
5709
6055
  this.element.src = config.src;
5710
6056
  if (config.type) {
5711
6057
  this.element.type = config.type;
@@ -5725,6 +6071,7 @@ var Player = class extends EventEmitter {
5725
6071
  }
5726
6072
  this.element.appendChild(track);
5727
6073
  });
6074
+ this.invalidateTrackCache();
5728
6075
  }
5729
6076
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5730
6077
  if (shouldChangeRenderer && this.renderer) {
@@ -5972,7 +6319,7 @@ var Player = class extends EventEmitter {
5972
6319
  }
5973
6320
  // Audio Description
5974
6321
  async enableAudioDescription() {
5975
- const hasSourceElementsWithDesc = Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6322
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
5976
6323
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
5977
6324
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
5978
6325
  console.warn("VidPly: No audio description source, source elements, or tracks provided");
@@ -5983,7 +6330,7 @@ var Player = class extends EventEmitter {
5983
6330
  let swappedTracksForTranscript = [];
5984
6331
  if (this.audioDescriptionSourceElement) {
5985
6332
  const currentSrc = this.element.currentSrc || this.element.src;
5986
- const sourceElements = Array.from(this.element.querySelectorAll("source"));
6333
+ const sourceElements = this.sourceElements;
5987
6334
  let sourceElementToUpdate = null;
5988
6335
  let descSrc = this.audioDescriptionSrc;
5989
6336
  for (const sourceEl of sourceElements) {
@@ -6080,8 +6427,9 @@ var Player = class extends EventEmitter {
6080
6427
  trackInfo.trackElement = newTrackElement;
6081
6428
  });
6082
6429
  this.element.load();
6430
+ this.invalidateTrackCache();
6083
6431
  const setupNewTracks = () => {
6084
- setTimeout(() => {
6432
+ this.setManagedTimeout(() => {
6085
6433
  swappedTracksForTranscript.forEach((trackInfo) => {
6086
6434
  const trackElement = trackInfo.trackElement;
6087
6435
  const newTextTrack = trackElement.track;
@@ -6117,7 +6465,7 @@ var Player = class extends EventEmitter {
6117
6465
  const skippedCount = validationResults.length - tracksToSwap.length;
6118
6466
  }
6119
6467
  }
6120
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
6468
+ const allSourceElements = this.sourceElements;
6121
6469
  const sourcesToUpdate = [];
6122
6470
  allSourceElements.forEach((sourceEl) => {
6123
6471
  const descSrcAttr = sourceEl.getAttribute("data-desc-src");
@@ -6295,7 +6643,7 @@ var Player = class extends EventEmitter {
6295
6643
  }, 100);
6296
6644
  }
6297
6645
  }
6298
- const fallbackSourceElements = Array.from(this.element.querySelectorAll("source"));
6646
+ const fallbackSourceElements = this.sourceElements;
6299
6647
  const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6300
6648
  if (hasSourceElementsWithDesc2) {
6301
6649
  const fallbackSourcesToUpdate = [];
@@ -6343,6 +6691,7 @@ var Player = class extends EventEmitter {
6343
6691
  this.element.appendChild(newSource);
6344
6692
  });
6345
6693
  this.element.load();
6694
+ this.invalidateTrackCache();
6346
6695
  } else {
6347
6696
  this.element.src = this.audioDescriptionSrc;
6348
6697
  }
@@ -6357,7 +6706,7 @@ var Player = class extends EventEmitter {
6357
6706
  if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6358
6707
  if (this.element.readyState >= 1) {
6359
6708
  this.element.currentTime = 1e-3;
6360
- setTimeout(() => {
6709
+ this.setManagedTimeout(() => {
6361
6710
  this.element.currentTime = 0;
6362
6711
  }, 10);
6363
6712
  }
@@ -6397,7 +6746,8 @@ var Player = class extends EventEmitter {
6397
6746
  const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6398
6747
  if (swappedTracks.length > 0) {
6399
6748
  const onMetadataLoaded = () => {
6400
- const allTextTracks = Array.from(this.element.textTracks);
6749
+ this.invalidateTrackCache();
6750
+ const allTextTracks = this.textTracks;
6401
6751
  const freshTracks = swappedTracks.map((trackInfo) => {
6402
6752
  const trackEl = trackInfo.trackElement;
6403
6753
  const expectedSrc = trackEl.getAttribute("src");
@@ -6407,9 +6757,7 @@ var Player = class extends EventEmitter {
6407
6757
  if (!foundTrack) {
6408
6758
  foundTrack = allTextTracks.find((track) => {
6409
6759
  if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6410
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6411
- (el) => el.track === track
6412
- );
6760
+ const trackElementForTrack = this.findTrackElement(track);
6413
6761
  if (trackElementForTrack) {
6414
6762
  const actualSrc = trackElementForTrack.getAttribute("src");
6415
6763
  if (actualSrc === expectedSrc) {
@@ -6421,9 +6769,7 @@ var Player = class extends EventEmitter {
6421
6769
  });
6422
6770
  }
6423
6771
  if (foundTrack) {
6424
- const trackElement = Array.from(this.element.querySelectorAll("track")).find(
6425
- (el) => el.track === foundTrack
6426
- );
6772
+ const trackElement = this.findTrackElement(foundTrack);
6427
6773
  if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6428
6774
  return null;
6429
6775
  }
@@ -6431,7 +6777,7 @@ var Player = class extends EventEmitter {
6431
6777
  return foundTrack;
6432
6778
  }).filter(Boolean);
6433
6779
  if (freshTracks.length === 0) {
6434
- setTimeout(() => {
6780
+ this.setManagedTimeout(() => {
6435
6781
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6436
6782
  this.transcriptManager.loadTranscriptData();
6437
6783
  }
@@ -6447,14 +6793,13 @@ var Player = class extends EventEmitter {
6447
6793
  const checkLoaded = () => {
6448
6794
  loadedCount++;
6449
6795
  if (loadedCount >= freshTracks.length) {
6450
- setTimeout(() => {
6796
+ this.setManagedTimeout(() => {
6451
6797
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6452
- const allTextTracks2 = Array.from(this.element.textTracks);
6798
+ this.invalidateTrackCache();
6799
+ const allTextTracks2 = this.textTracks;
6453
6800
  const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6454
6801
  const hasCorrectTracks = freshTracks.some((track) => {
6455
- const trackEl = Array.from(this.element.querySelectorAll("track")).find(
6456
- (el) => el.track === track
6457
- );
6802
+ const trackEl = this.findTrackElement(track);
6458
6803
  return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6459
6804
  });
6460
6805
  if (hasCorrectTracks || freshTracks.length > 0) {
@@ -6468,9 +6813,7 @@ var Player = class extends EventEmitter {
6468
6813
  if (track.mode === "disabled") {
6469
6814
  track.mode = "hidden";
6470
6815
  }
6471
- const trackElementForTrack = Array.from(this.element.querySelectorAll("track")).find(
6472
- (el) => el.track === track
6473
- );
6816
+ const trackElementForTrack = this.findTrackElement(track);
6474
6817
  const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6475
6818
  const expectedTrackInfo = swappedTracks.find((t) => {
6476
6819
  const tEl = t.trackElement;
@@ -6488,10 +6831,10 @@ var Player = class extends EventEmitter {
6488
6831
  track.mode = "hidden";
6489
6832
  }
6490
6833
  const onTrackLoad = () => {
6491
- setTimeout(checkLoaded, 300);
6834
+ this.setManagedTimeout(checkLoaded, 300);
6492
6835
  };
6493
6836
  if (track.readyState >= 2) {
6494
- setTimeout(() => {
6837
+ this.setManagedTimeout(() => {
6495
6838
  if (track.cues && track.cues.length > 0) {
6496
6839
  checkLoaded();
6497
6840
  } else {
@@ -6508,12 +6851,12 @@ var Player = class extends EventEmitter {
6508
6851
  });
6509
6852
  };
6510
6853
  const waitForTracks = () => {
6511
- setTimeout(() => {
6854
+ this.setManagedTimeout(() => {
6512
6855
  if (this.element.readyState >= 1) {
6513
6856
  onMetadataLoaded();
6514
6857
  } else {
6515
6858
  this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6516
- setTimeout(onMetadataLoaded, 2e3);
6859
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6517
6860
  }
6518
6861
  }, 500);
6519
6862
  };
@@ -6547,7 +6890,7 @@ var Player = class extends EventEmitter {
6547
6890
  }
6548
6891
  });
6549
6892
  }
6550
- const allSourceElements = Array.from(this.element.querySelectorAll("source"));
6893
+ const allSourceElements = this.sourceElements;
6551
6894
  const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6552
6895
  if (hasSourceElementsToSwap) {
6553
6896
  const sourcesToRestore = [];
@@ -6610,7 +6953,7 @@ var Player = class extends EventEmitter {
6610
6953
  this.play();
6611
6954
  }
6612
6955
  if (this.transcriptManager && this.transcriptManager.isVisible) {
6613
- setTimeout(() => {
6956
+ this.setManagedTimeout(() => {
6614
6957
  if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6615
6958
  this.transcriptManager.loadTranscriptData();
6616
6959
  }
@@ -6620,16 +6963,37 @@ var Player = class extends EventEmitter {
6620
6963
  this.emit("audiodescriptiondisabled");
6621
6964
  }
6622
6965
  async toggleAudioDescription() {
6623
- const textTracks = Array.from(this.element.textTracks || []);
6624
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
6625
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || Array.from(this.element.querySelectorAll("source")).some((el) => el.getAttribute("data-desc-src"));
6966
+ const descriptionTrack = this.findTextTrack("descriptions");
6967
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6626
6968
  if (descriptionTrack && hasAudioDescriptionSrc) {
6627
6969
  if (this.state.audioDescriptionEnabled) {
6628
6970
  descriptionTrack.mode = "hidden";
6629
6971
  await this.disableAudioDescription();
6630
6972
  } else {
6631
6973
  await this.enableAudioDescription();
6632
- descriptionTrack.mode = "showing";
6974
+ const enableDescriptionTrack = () => {
6975
+ this.invalidateTrackCache();
6976
+ const descTrack = this.findTextTrack("descriptions");
6977
+ if (descTrack) {
6978
+ if (descTrack.mode === "disabled") {
6979
+ descTrack.mode = "hidden";
6980
+ this.setManagedTimeout(() => {
6981
+ descTrack.mode = "showing";
6982
+ }, 50);
6983
+ } else {
6984
+ descTrack.mode = "showing";
6985
+ }
6986
+ } else if (this.element.readyState < 2) {
6987
+ this.setManagedTimeout(enableDescriptionTrack, 100);
6988
+ }
6989
+ };
6990
+ if (this.element.readyState >= 1) {
6991
+ this.setManagedTimeout(enableDescriptionTrack, 200);
6992
+ } else {
6993
+ this.element.addEventListener("loadedmetadata", () => {
6994
+ this.setManagedTimeout(enableDescriptionTrack, 200);
6995
+ }, { once: true });
6996
+ }
6633
6997
  }
6634
6998
  } else if (descriptionTrack) {
6635
6999
  if (descriptionTrack.mode === "showing") {
@@ -7042,9 +7406,25 @@ var Player = class extends EventEmitter {
7042
7406
  }
7043
7407
  }
7044
7408
  // Logging
7045
- log(message, type = "log") {
7046
- if (this.options.debug) {
7047
- console[type](`[VidPly]`, message);
7409
+ log(...messages) {
7410
+ if (!this.options.debug) {
7411
+ return;
7412
+ }
7413
+ let type = "log";
7414
+ if (messages.length > 0) {
7415
+ const potentialType = messages[messages.length - 1];
7416
+ if (typeof potentialType === "string" && console[potentialType]) {
7417
+ type = potentialType;
7418
+ messages = messages.slice(0, -1);
7419
+ }
7420
+ }
7421
+ if (messages.length === 0) {
7422
+ messages = [""];
7423
+ }
7424
+ if (typeof console[type] === "function") {
7425
+ console[type]("[VidPly]", ...messages);
7426
+ } else {
7427
+ console.log("[VidPly]", ...messages);
7048
7428
  }
7049
7429
  }
7050
7430
  // Setup responsive handlers
@@ -7104,7 +7484,7 @@ var Player = class extends EventEmitter {
7104
7484
  this.controlBar.updateFullscreenButton();
7105
7485
  }
7106
7486
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
7107
- setTimeout(() => {
7487
+ this.setManagedTimeout(() => {
7108
7488
  requestAnimationFrame(() => {
7109
7489
  this.storage.saveSignLanguagePreferences({ size: null });
7110
7490
  this.signLanguageDesiredPosition = "bottom-right";
@@ -7167,12 +7547,368 @@ var Player = class extends EventEmitter {
7167
7547
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
7168
7548
  this.fullscreenChangeHandler = null;
7169
7549
  }
7550
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
7551
+ this.timeouts.clear();
7552
+ if (this.metadataCueChangeHandler) {
7553
+ const textTracks = this.textTracks;
7554
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7555
+ if (metadataTrack) {
7556
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7557
+ }
7558
+ this.metadataCueChangeHandler = null;
7559
+ }
7560
+ if (this.metadataAlertHandlers && this.metadataAlertHandlers.size > 0) {
7561
+ this.metadataAlertHandlers.forEach(({ button, handler }) => {
7562
+ if (button && handler) {
7563
+ button.removeEventListener("click", handler);
7564
+ }
7565
+ });
7566
+ this.metadataAlertHandlers.clear();
7567
+ }
7170
7568
  if (this.container && this.container.parentNode) {
7171
7569
  this.container.parentNode.insertBefore(this.element, this.container);
7172
7570
  this.container.parentNode.removeChild(this.container);
7173
7571
  }
7174
7572
  this.removeAllListeners();
7175
7573
  }
7574
+ /**
7575
+ * Setup metadata track handling
7576
+ * This enables metadata tracks and listens for cue changes to trigger actions
7577
+ */
7578
+ setupMetadataHandling() {
7579
+ const setupMetadata = () => {
7580
+ const textTracks = this.textTracks;
7581
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
7582
+ if (metadataTrack) {
7583
+ if (metadataTrack.mode === "disabled") {
7584
+ metadataTrack.mode = "hidden";
7585
+ }
7586
+ if (this.metadataCueChangeHandler) {
7587
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
7588
+ }
7589
+ this.metadataCueChangeHandler = () => {
7590
+ const activeCues = Array.from(metadataTrack.activeCues || []);
7591
+ if (activeCues.length > 0) {
7592
+ if (this.options.debug) {
7593
+ this.log("[Metadata] Active cues:", activeCues.map((c) => ({
7594
+ start: c.startTime,
7595
+ end: c.endTime,
7596
+ text: c.text
7597
+ })));
7598
+ }
7599
+ }
7600
+ activeCues.forEach((cue) => {
7601
+ this.handleMetadataCue(cue);
7602
+ });
7603
+ };
7604
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
7605
+ if (this.options.debug) {
7606
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
7607
+ this.log("[Metadata] Track enabled,", cueCount, "cues available");
7608
+ }
7609
+ } else if (this.options.debug) {
7610
+ this.log("[Metadata] No metadata track found");
7611
+ }
7612
+ };
7613
+ setupMetadata();
7614
+ this.on("loadedmetadata", setupMetadata);
7615
+ }
7616
+ normalizeMetadataSelector(selector) {
7617
+ if (!selector) {
7618
+ return null;
7619
+ }
7620
+ const trimmed = selector.trim();
7621
+ if (!trimmed) {
7622
+ return null;
7623
+ }
7624
+ if (trimmed.startsWith("#") || trimmed.startsWith(".") || trimmed.startsWith("[")) {
7625
+ return trimmed;
7626
+ }
7627
+ return `#${trimmed}`;
7628
+ }
7629
+ resolveMetadataConfig(map, key) {
7630
+ if (!map || !key) {
7631
+ return null;
7632
+ }
7633
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
7634
+ return map[key];
7635
+ }
7636
+ const withoutHash = key.replace(/^#/, "");
7637
+ if (Object.prototype.hasOwnProperty.call(map, withoutHash)) {
7638
+ return map[withoutHash];
7639
+ }
7640
+ return null;
7641
+ }
7642
+ cacheMetadataAlertContent(element, config = {}) {
7643
+ if (!element) {
7644
+ return;
7645
+ }
7646
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7647
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7648
+ const titleEl = element.querySelector(titleSelector);
7649
+ if (titleEl && !titleEl.dataset.vidplyAlertTitleOriginal) {
7650
+ titleEl.dataset.vidplyAlertTitleOriginal = titleEl.textContent.trim();
7651
+ }
7652
+ const messageEl = element.querySelector(messageSelector);
7653
+ if (messageEl && !messageEl.dataset.vidplyAlertMessageOriginal) {
7654
+ messageEl.dataset.vidplyAlertMessageOriginal = messageEl.textContent.trim();
7655
+ }
7656
+ }
7657
+ restoreMetadataAlertContent(element, config = {}) {
7658
+ if (!element) {
7659
+ return;
7660
+ }
7661
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7662
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7663
+ const titleEl = element.querySelector(titleSelector);
7664
+ if (titleEl && titleEl.dataset.vidplyAlertTitleOriginal) {
7665
+ titleEl.textContent = titleEl.dataset.vidplyAlertTitleOriginal;
7666
+ }
7667
+ const messageEl = element.querySelector(messageSelector);
7668
+ if (messageEl && messageEl.dataset.vidplyAlertMessageOriginal) {
7669
+ messageEl.textContent = messageEl.dataset.vidplyAlertMessageOriginal;
7670
+ }
7671
+ }
7672
+ focusMetadataTarget(target, fallbackElement = null) {
7673
+ var _a, _b;
7674
+ if (!target || target === "none") {
7675
+ return;
7676
+ }
7677
+ if (target === "alert" && fallbackElement) {
7678
+ fallbackElement.focus();
7679
+ return;
7680
+ }
7681
+ if (target === "player") {
7682
+ if (this.container) {
7683
+ this.container.focus();
7684
+ }
7685
+ return;
7686
+ }
7687
+ if (target === "media") {
7688
+ this.element.focus();
7689
+ return;
7690
+ }
7691
+ if (target === "playButton") {
7692
+ const playButton = (_b = (_a = this.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.playPause;
7693
+ if (playButton) {
7694
+ playButton.focus();
7695
+ }
7696
+ return;
7697
+ }
7698
+ if (typeof target === "string") {
7699
+ const targetElement = document.querySelector(target);
7700
+ if (targetElement) {
7701
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
7702
+ targetElement.setAttribute("tabindex", "-1");
7703
+ }
7704
+ targetElement.focus();
7705
+ }
7706
+ }
7707
+ }
7708
+ handleMetadataAlert(selector, options = {}) {
7709
+ if (!selector) {
7710
+ return;
7711
+ }
7712
+ const config = this.resolveMetadataConfig(this.options.metadataAlerts, selector) || {};
7713
+ const element = options.element || document.querySelector(selector);
7714
+ if (!element) {
7715
+ if (this.options.debug) {
7716
+ this.log("[Metadata] Alert element not found:", selector);
7717
+ }
7718
+ return;
7719
+ }
7720
+ if (this.options.debug) {
7721
+ this.log("[Metadata] Handling alert", selector, { reason: options.reason, config });
7722
+ }
7723
+ this.cacheMetadataAlertContent(element, config);
7724
+ if (!element.dataset.vidplyAlertOriginalDisplay) {
7725
+ element.dataset.vidplyAlertOriginalDisplay = element.style.display || "";
7726
+ }
7727
+ if (!element.dataset.vidplyAlertDisplay) {
7728
+ element.dataset.vidplyAlertDisplay = config.display || "block";
7729
+ }
7730
+ const shouldShow = options.show !== void 0 ? options.show : config.show !== false;
7731
+ if (shouldShow) {
7732
+ const displayValue = config.display || element.dataset.vidplyAlertDisplay || "block";
7733
+ element.style.display = displayValue;
7734
+ element.hidden = false;
7735
+ element.removeAttribute("hidden");
7736
+ element.setAttribute("aria-hidden", "false");
7737
+ element.setAttribute("data-vidply-alert-active", "true");
7738
+ }
7739
+ const shouldReset = config.resetContent !== false && options.reason === "focus";
7740
+ if (shouldReset) {
7741
+ this.restoreMetadataAlertContent(element, config);
7742
+ }
7743
+ const shouldFocus = options.focus !== void 0 ? options.focus : config.focusOnShow ?? options.reason !== "focus";
7744
+ if (shouldShow && shouldFocus) {
7745
+ if (element.tabIndex === -1 && !element.hasAttribute("tabindex")) {
7746
+ element.setAttribute("tabindex", "-1");
7747
+ }
7748
+ element.focus();
7749
+ }
7750
+ if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
7751
+ element.scrollIntoView({ behavior: "smooth", block: "nearest" });
7752
+ }
7753
+ const continueSelector = config.continueButton;
7754
+ if (continueSelector) {
7755
+ let continueButton = null;
7756
+ if (continueSelector === "self") {
7757
+ continueButton = element;
7758
+ } else if (element.matches(continueSelector)) {
7759
+ continueButton = element;
7760
+ } else {
7761
+ continueButton = element.querySelector(continueSelector) || document.querySelector(continueSelector);
7762
+ }
7763
+ if (continueButton && !this.metadataAlertHandlers.has(selector)) {
7764
+ const handler = () => {
7765
+ const hideOnContinue = config.hideOnContinue !== false;
7766
+ if (hideOnContinue) {
7767
+ const originalDisplay = element.dataset.vidplyAlertOriginalDisplay || "";
7768
+ element.style.display = config.hideDisplay || originalDisplay || "none";
7769
+ element.setAttribute("aria-hidden", "true");
7770
+ element.removeAttribute("data-vidply-alert-active");
7771
+ }
7772
+ if (config.resume !== false && this.state.paused) {
7773
+ this.play();
7774
+ }
7775
+ const focusTarget = config.focusTarget || "playButton";
7776
+ this.setManagedTimeout(() => {
7777
+ this.focusMetadataTarget(focusTarget, element);
7778
+ }, config.focusDelay ?? 100);
7779
+ };
7780
+ continueButton.addEventListener("click", handler);
7781
+ this.metadataAlertHandlers.set(selector, { button: continueButton, handler });
7782
+ }
7783
+ }
7784
+ return element;
7785
+ }
7786
+ handleMetadataHashtags(hashtags) {
7787
+ if (!Array.isArray(hashtags) || hashtags.length === 0) {
7788
+ return;
7789
+ }
7790
+ const configMap = this.options.metadataHashtags;
7791
+ if (!configMap) {
7792
+ return;
7793
+ }
7794
+ hashtags.forEach((tag) => {
7795
+ const config = this.resolveMetadataConfig(configMap, tag);
7796
+ if (!config) {
7797
+ return;
7798
+ }
7799
+ const selector = this.normalizeMetadataSelector(config.alert || config.selector || config.target);
7800
+ if (!selector) {
7801
+ return;
7802
+ }
7803
+ const element = document.querySelector(selector);
7804
+ if (!element) {
7805
+ if (this.options.debug) {
7806
+ this.log("[Metadata] Hashtag target not found:", selector);
7807
+ }
7808
+ return;
7809
+ }
7810
+ if (this.options.debug) {
7811
+ this.log("[Metadata] Handling hashtag", tag, { selector, config });
7812
+ }
7813
+ this.cacheMetadataAlertContent(element, config);
7814
+ if (config.title) {
7815
+ const titleSelector = config.titleSelector || "[data-vidply-alert-title], h3, header";
7816
+ const titleEl = element.querySelector(titleSelector);
7817
+ if (titleEl) {
7818
+ titleEl.textContent = config.title;
7819
+ }
7820
+ }
7821
+ if (config.message) {
7822
+ const messageSelector = config.messageSelector || "[data-vidply-alert-message], p";
7823
+ const messageEl = element.querySelector(messageSelector);
7824
+ if (messageEl) {
7825
+ messageEl.textContent = config.message;
7826
+ }
7827
+ }
7828
+ const show = config.show !== false;
7829
+ const focus = config.focus !== void 0 ? config.focus : false;
7830
+ this.handleMetadataAlert(selector, {
7831
+ element,
7832
+ show,
7833
+ focus,
7834
+ autoScroll: config.autoScroll,
7835
+ reason: "hashtag"
7836
+ });
7837
+ });
7838
+ }
7839
+ /**
7840
+ * Handle individual metadata cues
7841
+ * Parses metadata text and emits events or triggers actions
7842
+ */
7843
+ handleMetadataCue(cue) {
7844
+ const text = cue.text.trim();
7845
+ if (this.options.debug) {
7846
+ this.log("[Metadata] Processing cue:", {
7847
+ time: cue.startTime,
7848
+ text
7849
+ });
7850
+ }
7851
+ this.emit("metadata", {
7852
+ time: cue.startTime,
7853
+ endTime: cue.endTime,
7854
+ text,
7855
+ cue
7856
+ });
7857
+ if (text.includes("PAUSE")) {
7858
+ if (!this.state.paused) {
7859
+ if (this.options.debug) {
7860
+ this.log("[Metadata] Pausing video at", cue.startTime);
7861
+ }
7862
+ this.pause();
7863
+ }
7864
+ this.emit("metadata:pause", { time: cue.startTime, text });
7865
+ }
7866
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
7867
+ if (focusMatch) {
7868
+ const targetSelector = focusMatch[1];
7869
+ const normalizedSelector = this.normalizeMetadataSelector(targetSelector);
7870
+ const targetElement = normalizedSelector ? document.querySelector(normalizedSelector) : null;
7871
+ if (targetElement) {
7872
+ if (this.options.debug) {
7873
+ this.log("[Metadata] Focusing element:", normalizedSelector);
7874
+ }
7875
+ if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
7876
+ targetElement.setAttribute("tabindex", "-1");
7877
+ }
7878
+ this.setManagedTimeout(() => {
7879
+ targetElement.focus();
7880
+ targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
7881
+ }, 10);
7882
+ } else if (this.options.debug) {
7883
+ this.log("[Metadata] Element not found:", normalizedSelector || targetSelector);
7884
+ }
7885
+ this.emit("metadata:focus", {
7886
+ time: cue.startTime,
7887
+ target: targetSelector,
7888
+ selector: normalizedSelector,
7889
+ element: targetElement,
7890
+ text
7891
+ });
7892
+ if (normalizedSelector) {
7893
+ this.handleMetadataAlert(normalizedSelector, {
7894
+ element: targetElement,
7895
+ reason: "focus"
7896
+ });
7897
+ }
7898
+ }
7899
+ const hashtags = text.match(/#[\w-]+/g);
7900
+ if (hashtags) {
7901
+ if (this.options.debug) {
7902
+ this.log("[Metadata] Hashtags found:", hashtags);
7903
+ }
7904
+ this.emit("metadata:hashtags", {
7905
+ time: cue.startTime,
7906
+ hashtags,
7907
+ text
7908
+ });
7909
+ this.handleMetadataHashtags(hashtags);
7910
+ }
7911
+ }
7176
7912
  };
7177
7913
  Player.instances = [];
7178
7914