vidply 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -5469,12 +5702,24 @@ var Player = class extends EventEmitter {
5469
5702
  this.audioDescriptionSrc = this.options.audioDescriptionSrc;
5470
5703
  this.signLanguageSrc = this.options.signLanguageSrc;
5471
5704
  this.signLanguageVideo = null;
5705
+ this.audioDescriptionSourceElement = null;
5706
+ this.originalAudioDescriptionSource = null;
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();
5472
5715
  this.container = null;
5473
5716
  this.renderer = null;
5474
5717
  this.controlBar = null;
5475
5718
  this.captionManager = null;
5476
5719
  this.keyboardManager = null;
5477
5720
  this.settingsDialog = null;
5721
+ this.metadataCueChangeHandler = null;
5722
+ this.metadataAlertHandlers = /* @__PURE__ */ new Map();
5478
5723
  this.init();
5479
5724
  }
5480
5725
  async init() {
@@ -5506,6 +5751,7 @@ var Player = class extends EventEmitter {
5506
5751
  if (this.options.transcript || this.options.transcriptButton) {
5507
5752
  this.transcriptManager = new TranscriptManager(this);
5508
5753
  }
5754
+ this.setupMetadataHandling();
5509
5755
  if (this.options.keyboard) {
5510
5756
  this.keyboardManager = new KeyboardManager(this);
5511
5757
  }
@@ -5591,6 +5837,8 @@ var Player = class extends EventEmitter {
5591
5837
  if (this.element.tagName === "VIDEO") {
5592
5838
  this.createPlayButtonOverlay();
5593
5839
  }
5840
+ this.element.vidply = this;
5841
+ _Player.instances.push(this);
5594
5842
  this.element.style.cursor = "pointer";
5595
5843
  this.element.addEventListener("click", (e) => {
5596
5844
  if (e.target === this.element) {
@@ -5623,6 +5871,53 @@ var Player = class extends EventEmitter {
5623
5871
  if (!src) {
5624
5872
  throw new Error("No media source found");
5625
5873
  }
5874
+ const sourceElements = this.sourceElements;
5875
+ for (const sourceEl of sourceElements) {
5876
+ const descSrc = sourceEl.getAttribute("data-desc-src");
5877
+ const origSrc = sourceEl.getAttribute("data-orig-src");
5878
+ if (descSrc || origSrc) {
5879
+ if (!this.audioDescriptionSourceElement) {
5880
+ this.audioDescriptionSourceElement = sourceEl;
5881
+ }
5882
+ if (origSrc) {
5883
+ if (!this.originalAudioDescriptionSource) {
5884
+ this.originalAudioDescriptionSource = origSrc;
5885
+ }
5886
+ if (!this.originalSrc) {
5887
+ this.originalSrc = origSrc;
5888
+ }
5889
+ } else {
5890
+ const currentSrcAttr = sourceEl.getAttribute("src");
5891
+ if (!this.originalAudioDescriptionSource && currentSrcAttr) {
5892
+ this.originalAudioDescriptionSource = currentSrcAttr;
5893
+ }
5894
+ if (!this.originalSrc && currentSrcAttr) {
5895
+ this.originalSrc = currentSrcAttr;
5896
+ }
5897
+ }
5898
+ if (descSrc && !this.audioDescriptionSrc) {
5899
+ this.audioDescriptionSrc = descSrc;
5900
+ }
5901
+ }
5902
+ }
5903
+ const trackElements = this.trackElements;
5904
+ trackElements.forEach((trackEl) => {
5905
+ const trackKind = trackEl.getAttribute("kind");
5906
+ const trackDescSrc = trackEl.getAttribute("data-desc-src");
5907
+ if (trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters") {
5908
+ if (trackDescSrc) {
5909
+ this.audioDescriptionCaptionTracks.push({
5910
+ trackElement: trackEl,
5911
+ originalSrc: trackEl.getAttribute("src"),
5912
+ describedSrc: trackDescSrc,
5913
+ originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
5914
+ explicit: true
5915
+ // Explicitly defined, so we should validate it
5916
+ });
5917
+ this.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute("src")} -> ${trackDescSrc}`);
5918
+ }
5919
+ }
5920
+ });
5626
5921
  if (!this.originalSrc) {
5627
5922
  this.originalSrc = src;
5628
5923
  }
@@ -5639,6 +5934,106 @@ var Player = class extends EventEmitter {
5639
5934
  this.log(`Using ${renderer.name} renderer`);
5640
5935
  this.renderer = new renderer(this);
5641
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
+ }
5642
6037
  }
5643
6038
  /**
5644
6039
  * Load new media source (for playlists)
@@ -5654,8 +6049,9 @@ var Player = class extends EventEmitter {
5654
6049
  if (this.renderer) {
5655
6050
  this.pause();
5656
6051
  }
5657
- const existingTracks = this.element.querySelectorAll("track");
6052
+ const existingTracks = this.trackElements;
5658
6053
  existingTracks.forEach((track) => track.remove());
6054
+ this.invalidateTrackCache();
5659
6055
  this.element.src = config.src;
5660
6056
  if (config.type) {
5661
6057
  this.element.type = config.type;
@@ -5675,6 +6071,7 @@ var Player = class extends EventEmitter {
5675
6071
  }
5676
6072
  this.element.appendChild(track);
5677
6073
  });
6074
+ this.invalidateTrackCache();
5678
6075
  }
5679
6076
  const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
5680
6077
  if (shouldChangeRenderer && this.renderer) {
@@ -5907,15 +6304,398 @@ var Player = class extends EventEmitter {
5907
6304
  this.enableCaptions();
5908
6305
  }
5909
6306
  }
6307
+ /**
6308
+ * Check if a track file exists
6309
+ * @param {string} url - Track file URL
6310
+ * @returns {Promise<boolean>} - True if file exists
6311
+ */
6312
+ async validateTrackExists(url) {
6313
+ try {
6314
+ const response = await fetch(url, { method: "HEAD", cache: "no-cache" });
6315
+ return response.ok;
6316
+ } catch (error) {
6317
+ return false;
6318
+ }
6319
+ }
5910
6320
  // Audio Description
5911
6321
  async enableAudioDescription() {
5912
- if (!this.audioDescriptionSrc) {
5913
- console.warn("VidPly: No audio description source provided");
6322
+ const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6323
+ const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
6324
+ if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
6325
+ console.warn("VidPly: No audio description source, source elements, or tracks provided");
5914
6326
  return;
5915
6327
  }
5916
6328
  const currentTime = this.state.currentTime;
5917
6329
  const wasPlaying = this.state.playing;
5918
- this.element.src = this.audioDescriptionSrc;
6330
+ let swappedTracksForTranscript = [];
6331
+ if (this.audioDescriptionSourceElement) {
6332
+ const currentSrc = this.element.currentSrc || this.element.src;
6333
+ const sourceElements = this.sourceElements;
6334
+ let sourceElementToUpdate = null;
6335
+ let descSrc = this.audioDescriptionSrc;
6336
+ for (const sourceEl of sourceElements) {
6337
+ const sourceSrc = sourceEl.getAttribute("src");
6338
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6339
+ const sourceFilename = sourceSrc ? sourceSrc.split("/").pop() : "";
6340
+ const currentFilename = currentSrc ? currentSrc.split("/").pop() : "";
6341
+ if (currentSrc && (currentSrc === sourceSrc || currentSrc.includes(sourceSrc) || currentSrc.includes(sourceFilename) || sourceFilename && currentFilename === sourceFilename)) {
6342
+ sourceElementToUpdate = sourceEl;
6343
+ if (descSrcAttr) {
6344
+ descSrc = descSrcAttr;
6345
+ } else if (sourceSrc) {
6346
+ descSrc = this.audioDescriptionSrc || descSrc;
6347
+ }
6348
+ break;
6349
+ }
6350
+ }
6351
+ if (!sourceElementToUpdate) {
6352
+ sourceElementToUpdate = this.audioDescriptionSourceElement;
6353
+ const storedDescSrc = sourceElementToUpdate.getAttribute("data-desc-src");
6354
+ if (storedDescSrc) {
6355
+ descSrc = storedDescSrc;
6356
+ }
6357
+ }
6358
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6359
+ const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
6360
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
6361
+ if (trackInfo.explicit === true) {
6362
+ try {
6363
+ const exists = await this.validateTrackExists(trackInfo.describedSrc);
6364
+ return { trackInfo, exists };
6365
+ } catch (error) {
6366
+ return { trackInfo, exists: false };
6367
+ }
6368
+ } else {
6369
+ return { trackInfo, exists: false };
6370
+ }
6371
+ }
6372
+ return { trackInfo, exists: false };
6373
+ });
6374
+ const validationResults = await Promise.all(validationPromises);
6375
+ const tracksToSwap = validationResults.filter((result) => result.exists);
6376
+ if (tracksToSwap.length > 0) {
6377
+ const trackModes = /* @__PURE__ */ new Map();
6378
+ tracksToSwap.forEach(({ trackInfo }) => {
6379
+ const textTrack = trackInfo.trackElement.track;
6380
+ if (textTrack) {
6381
+ trackModes.set(trackInfo, {
6382
+ wasShowing: textTrack.mode === "showing",
6383
+ wasHidden: textTrack.mode === "hidden"
6384
+ });
6385
+ } else {
6386
+ trackModes.set(trackInfo, {
6387
+ wasShowing: false,
6388
+ wasHidden: false
6389
+ });
6390
+ }
6391
+ });
6392
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
6393
+ const oldSrc = trackInfo.trackElement.getAttribute("src");
6394
+ const parent = trackInfo.trackElement.parentNode;
6395
+ const nextSibling = trackInfo.trackElement.nextSibling;
6396
+ const attributes = {};
6397
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
6398
+ attributes[attr.name] = attr.value;
6399
+ });
6400
+ return {
6401
+ trackInfo,
6402
+ oldSrc,
6403
+ parent,
6404
+ nextSibling,
6405
+ attributes
6406
+ };
6407
+ });
6408
+ tracksToReadd.forEach(({ trackInfo }) => {
6409
+ trackInfo.trackElement.remove();
6410
+ });
6411
+ this.element.load();
6412
+ setTimeout(() => {
6413
+ tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
6414
+ swappedTracksForTranscript.push(trackInfo);
6415
+ const newTrackElement = document.createElement("track");
6416
+ newTrackElement.setAttribute("src", trackInfo.describedSrc);
6417
+ Object.keys(attributes).forEach((attrName) => {
6418
+ if (attrName !== "src" && attrName !== "data-desc-src") {
6419
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
6420
+ }
6421
+ });
6422
+ if (nextSibling && nextSibling.parentNode) {
6423
+ parent.insertBefore(newTrackElement, nextSibling);
6424
+ } else {
6425
+ parent.appendChild(newTrackElement);
6426
+ }
6427
+ trackInfo.trackElement = newTrackElement;
6428
+ });
6429
+ this.element.load();
6430
+ this.invalidateTrackCache();
6431
+ const setupNewTracks = () => {
6432
+ this.setManagedTimeout(() => {
6433
+ swappedTracksForTranscript.forEach((trackInfo) => {
6434
+ const trackElement = trackInfo.trackElement;
6435
+ const newTextTrack = trackElement.track;
6436
+ if (newTextTrack) {
6437
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
6438
+ newTextTrack.mode = "hidden";
6439
+ const restoreMode = () => {
6440
+ if (modeInfo.wasShowing) {
6441
+ newTextTrack.mode = "hidden";
6442
+ } else if (modeInfo.wasHidden) {
6443
+ newTextTrack.mode = "hidden";
6444
+ } else {
6445
+ newTextTrack.mode = "disabled";
6446
+ }
6447
+ };
6448
+ if (newTextTrack.readyState >= 2) {
6449
+ restoreMode();
6450
+ } else {
6451
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
6452
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
6453
+ }
6454
+ }
6455
+ });
6456
+ }, 300);
6457
+ };
6458
+ if (this.element.readyState >= 1) {
6459
+ setTimeout(setupNewTracks, 200);
6460
+ } else {
6461
+ this.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
6462
+ setTimeout(setupNewTracks, 2e3);
6463
+ }
6464
+ }, 100);
6465
+ const skippedCount = validationResults.length - tracksToSwap.length;
6466
+ }
6467
+ }
6468
+ const allSourceElements = this.sourceElements;
6469
+ const sourcesToUpdate = [];
6470
+ allSourceElements.forEach((sourceEl) => {
6471
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6472
+ const currentSrc2 = sourceEl.getAttribute("src");
6473
+ if (descSrcAttr) {
6474
+ const type = sourceEl.getAttribute("type");
6475
+ let origSrc = sourceEl.getAttribute("data-orig-src");
6476
+ if (!origSrc) {
6477
+ origSrc = currentSrc2;
6478
+ }
6479
+ sourcesToUpdate.push({
6480
+ src: descSrcAttr,
6481
+ // Use described version
6482
+ type,
6483
+ origSrc,
6484
+ descSrc: descSrcAttr
6485
+ });
6486
+ } else {
6487
+ const type = sourceEl.getAttribute("type");
6488
+ const src = sourceEl.getAttribute("src");
6489
+ sourcesToUpdate.push({
6490
+ src,
6491
+ type,
6492
+ origSrc: null,
6493
+ descSrc: null
6494
+ });
6495
+ }
6496
+ });
6497
+ allSourceElements.forEach((sourceEl) => {
6498
+ sourceEl.remove();
6499
+ });
6500
+ sourcesToUpdate.forEach((sourceInfo) => {
6501
+ const newSource = document.createElement("source");
6502
+ newSource.setAttribute("src", sourceInfo.src);
6503
+ if (sourceInfo.type) {
6504
+ newSource.setAttribute("type", sourceInfo.type);
6505
+ }
6506
+ if (sourceInfo.origSrc) {
6507
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6508
+ }
6509
+ if (sourceInfo.descSrc) {
6510
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6511
+ }
6512
+ this.element.appendChild(newSource);
6513
+ });
6514
+ this.element.load();
6515
+ await new Promise((resolve) => {
6516
+ const onLoadedMetadata = () => {
6517
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
6518
+ resolve();
6519
+ };
6520
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
6521
+ });
6522
+ await new Promise((resolve) => setTimeout(resolve, 300));
6523
+ if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6524
+ if (this.element.readyState >= 1) {
6525
+ this.element.currentTime = 1e-3;
6526
+ setTimeout(() => {
6527
+ this.element.currentTime = 0;
6528
+ }, 10);
6529
+ }
6530
+ }
6531
+ this.seek(currentTime);
6532
+ if (wasPlaying) {
6533
+ this.play();
6534
+ }
6535
+ this.state.audioDescriptionEnabled = true;
6536
+ this.emit("audiodescriptionenabled");
6537
+ } else {
6538
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6539
+ const validationPromises = this.audioDescriptionCaptionTracks.map(async (trackInfo) => {
6540
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
6541
+ if (trackInfo.explicit === true) {
6542
+ try {
6543
+ const exists = await this.validateTrackExists(trackInfo.describedSrc);
6544
+ return { trackInfo, exists };
6545
+ } catch (error) {
6546
+ return { trackInfo, exists: false };
6547
+ }
6548
+ } else {
6549
+ return { trackInfo, exists: false };
6550
+ }
6551
+ }
6552
+ return { trackInfo, exists: false };
6553
+ });
6554
+ const validationResults = await Promise.all(validationPromises);
6555
+ const tracksToSwap = validationResults.filter((result) => result.exists);
6556
+ if (tracksToSwap.length > 0) {
6557
+ const trackModes = /* @__PURE__ */ new Map();
6558
+ tracksToSwap.forEach(({ trackInfo }) => {
6559
+ const textTrack = trackInfo.trackElement.track;
6560
+ if (textTrack) {
6561
+ trackModes.set(trackInfo, {
6562
+ wasShowing: textTrack.mode === "showing",
6563
+ wasHidden: textTrack.mode === "hidden"
6564
+ });
6565
+ } else {
6566
+ trackModes.set(trackInfo, {
6567
+ wasShowing: false,
6568
+ wasHidden: false
6569
+ });
6570
+ }
6571
+ });
6572
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
6573
+ const oldSrc = trackInfo.trackElement.getAttribute("src");
6574
+ const parent = trackInfo.trackElement.parentNode;
6575
+ const nextSibling = trackInfo.trackElement.nextSibling;
6576
+ const attributes = {};
6577
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
6578
+ attributes[attr.name] = attr.value;
6579
+ });
6580
+ return {
6581
+ trackInfo,
6582
+ oldSrc,
6583
+ parent,
6584
+ nextSibling,
6585
+ attributes
6586
+ };
6587
+ });
6588
+ tracksToReadd.forEach(({ trackInfo }) => {
6589
+ trackInfo.trackElement.remove();
6590
+ });
6591
+ this.element.load();
6592
+ setTimeout(() => {
6593
+ tracksToReadd.forEach(({ trackInfo, oldSrc, parent, nextSibling, attributes }) => {
6594
+ swappedTracksForTranscript.push(trackInfo);
6595
+ const newTrackElement = document.createElement("track");
6596
+ newTrackElement.setAttribute("src", trackInfo.describedSrc);
6597
+ Object.keys(attributes).forEach((attrName) => {
6598
+ if (attrName !== "src" && attrName !== "data-desc-src") {
6599
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
6600
+ }
6601
+ });
6602
+ if (nextSibling && nextSibling.parentNode) {
6603
+ parent.insertBefore(newTrackElement, nextSibling);
6604
+ } else {
6605
+ parent.appendChild(newTrackElement);
6606
+ }
6607
+ trackInfo.trackElement = newTrackElement;
6608
+ });
6609
+ this.element.load();
6610
+ const setupNewTracks = () => {
6611
+ setTimeout(() => {
6612
+ swappedTracksForTranscript.forEach((trackInfo) => {
6613
+ const trackElement = trackInfo.trackElement;
6614
+ const newTextTrack = trackElement.track;
6615
+ if (newTextTrack) {
6616
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
6617
+ newTextTrack.mode = "hidden";
6618
+ const restoreMode = () => {
6619
+ if (modeInfo.wasShowing) {
6620
+ newTextTrack.mode = "hidden";
6621
+ } else if (modeInfo.wasHidden) {
6622
+ newTextTrack.mode = "hidden";
6623
+ } else {
6624
+ newTextTrack.mode = "disabled";
6625
+ }
6626
+ };
6627
+ if (newTextTrack.readyState >= 2) {
6628
+ restoreMode();
6629
+ } else {
6630
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
6631
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
6632
+ }
6633
+ }
6634
+ });
6635
+ }, 300);
6636
+ };
6637
+ if (this.element.readyState >= 1) {
6638
+ setTimeout(setupNewTracks, 200);
6639
+ } else {
6640
+ this.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
6641
+ setTimeout(setupNewTracks, 2e3);
6642
+ }
6643
+ }, 100);
6644
+ }
6645
+ }
6646
+ const fallbackSourceElements = this.sourceElements;
6647
+ const hasSourceElementsWithDesc2 = fallbackSourceElements.some((el) => el.getAttribute("data-desc-src"));
6648
+ if (hasSourceElementsWithDesc2) {
6649
+ const fallbackSourcesToUpdate = [];
6650
+ fallbackSourceElements.forEach((sourceEl) => {
6651
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6652
+ const currentSrc = sourceEl.getAttribute("src");
6653
+ if (descSrcAttr) {
6654
+ const type = sourceEl.getAttribute("type");
6655
+ let origSrc = sourceEl.getAttribute("data-orig-src");
6656
+ if (!origSrc) {
6657
+ origSrc = currentSrc;
6658
+ }
6659
+ fallbackSourcesToUpdate.push({
6660
+ src: descSrcAttr,
6661
+ type,
6662
+ origSrc,
6663
+ descSrc: descSrcAttr
6664
+ });
6665
+ } else {
6666
+ const type = sourceEl.getAttribute("type");
6667
+ const src = sourceEl.getAttribute("src");
6668
+ fallbackSourcesToUpdate.push({
6669
+ src,
6670
+ type,
6671
+ origSrc: null,
6672
+ descSrc: null
6673
+ });
6674
+ }
6675
+ });
6676
+ fallbackSourceElements.forEach((sourceEl) => {
6677
+ sourceEl.remove();
6678
+ });
6679
+ fallbackSourcesToUpdate.forEach((sourceInfo) => {
6680
+ const newSource = document.createElement("source");
6681
+ newSource.setAttribute("src", sourceInfo.src);
6682
+ if (sourceInfo.type) {
6683
+ newSource.setAttribute("type", sourceInfo.type);
6684
+ }
6685
+ if (sourceInfo.origSrc) {
6686
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6687
+ }
6688
+ if (sourceInfo.descSrc) {
6689
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6690
+ }
6691
+ this.element.appendChild(newSource);
6692
+ });
6693
+ this.element.load();
6694
+ this.invalidateTrackCache();
6695
+ } else {
6696
+ this.element.src = this.audioDescriptionSrc;
6697
+ }
6698
+ }
5919
6699
  await new Promise((resolve) => {
5920
6700
  const onLoadedMetadata = () => {
5921
6701
  this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
@@ -5923,10 +6703,177 @@ var Player = class extends EventEmitter {
5923
6703
  };
5924
6704
  this.element.addEventListener("loadedmetadata", onLoadedMetadata);
5925
6705
  });
6706
+ if (this.element.tagName === "VIDEO" && currentTime === 0 && !wasPlaying) {
6707
+ if (this.element.readyState >= 1) {
6708
+ this.element.currentTime = 1e-3;
6709
+ this.setManagedTimeout(() => {
6710
+ this.element.currentTime = 0;
6711
+ }, 10);
6712
+ }
6713
+ }
5926
6714
  this.seek(currentTime);
5927
6715
  if (wasPlaying) {
5928
6716
  this.play();
5929
6717
  }
6718
+ if (swappedTracksForTranscript.length > 0 && this.captionManager) {
6719
+ const wasCaptionsEnabled = this.state.captionsEnabled;
6720
+ let currentTrackInfo = null;
6721
+ if (this.captionManager.currentTrack) {
6722
+ const currentTrackIndex = this.captionManager.tracks.findIndex((t) => t.track === this.captionManager.currentTrack.track);
6723
+ if (currentTrackIndex >= 0) {
6724
+ currentTrackInfo = {
6725
+ language: this.captionManager.tracks[currentTrackIndex].language,
6726
+ kind: this.captionManager.tracks[currentTrackIndex].kind
6727
+ };
6728
+ }
6729
+ }
6730
+ setTimeout(() => {
6731
+ this.captionManager.tracks = [];
6732
+ this.captionManager.loadTracks();
6733
+ if (wasCaptionsEnabled && currentTrackInfo && this.captionManager.tracks.length > 0) {
6734
+ const matchingTrackIndex = this.captionManager.tracks.findIndex(
6735
+ (t) => t.language === currentTrackInfo.language && t.kind === currentTrackInfo.kind
6736
+ );
6737
+ if (matchingTrackIndex >= 0) {
6738
+ this.captionManager.enable(matchingTrackIndex);
6739
+ } else if (this.captionManager.tracks.length > 0) {
6740
+ this.captionManager.enable(0);
6741
+ }
6742
+ }
6743
+ }, 600);
6744
+ }
6745
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
6746
+ const swappedTracks = typeof swappedTracksForTranscript !== "undefined" ? swappedTracksForTranscript : [];
6747
+ if (swappedTracks.length > 0) {
6748
+ const onMetadataLoaded = () => {
6749
+ this.invalidateTrackCache();
6750
+ const allTextTracks = this.textTracks;
6751
+ const freshTracks = swappedTracks.map((trackInfo) => {
6752
+ const trackEl = trackInfo.trackElement;
6753
+ const expectedSrc = trackEl.getAttribute("src");
6754
+ const srclang = trackEl.getAttribute("srclang");
6755
+ const kind = trackEl.getAttribute("kind");
6756
+ let foundTrack = allTextTracks.find((track) => trackEl.track === track);
6757
+ if (!foundTrack) {
6758
+ foundTrack = allTextTracks.find((track) => {
6759
+ if (track.language === srclang && (track.kind === kind || kind === "captions" && track.kind === "subtitles")) {
6760
+ const trackElementForTrack = this.findTrackElement(track);
6761
+ if (trackElementForTrack) {
6762
+ const actualSrc = trackElementForTrack.getAttribute("src");
6763
+ if (actualSrc === expectedSrc) {
6764
+ return true;
6765
+ }
6766
+ }
6767
+ }
6768
+ return false;
6769
+ });
6770
+ }
6771
+ if (foundTrack) {
6772
+ const trackElement = this.findTrackElement(foundTrack);
6773
+ if (trackElement && trackElement.getAttribute("src") !== expectedSrc) {
6774
+ return null;
6775
+ }
6776
+ }
6777
+ return foundTrack;
6778
+ }).filter(Boolean);
6779
+ if (freshTracks.length === 0) {
6780
+ this.setManagedTimeout(() => {
6781
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6782
+ this.transcriptManager.loadTranscriptData();
6783
+ }
6784
+ }, 1e3);
6785
+ return;
6786
+ }
6787
+ freshTracks.forEach((track) => {
6788
+ if (track.mode === "disabled") {
6789
+ track.mode = "hidden";
6790
+ }
6791
+ });
6792
+ let loadedCount = 0;
6793
+ const checkLoaded = () => {
6794
+ loadedCount++;
6795
+ if (loadedCount >= freshTracks.length) {
6796
+ this.setManagedTimeout(() => {
6797
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6798
+ this.invalidateTrackCache();
6799
+ const allTextTracks2 = this.textTracks;
6800
+ const swappedTrackSrcs = swappedTracks.map((t) => t.describedSrc);
6801
+ const hasCorrectTracks = freshTracks.some((track) => {
6802
+ const trackEl = this.findTrackElement(track);
6803
+ return trackEl && swappedTrackSrcs.includes(trackEl.getAttribute("src"));
6804
+ });
6805
+ if (hasCorrectTracks || freshTracks.length > 0) {
6806
+ this.transcriptManager.loadTranscriptData();
6807
+ }
6808
+ }
6809
+ }, 800);
6810
+ }
6811
+ };
6812
+ freshTracks.forEach((track) => {
6813
+ if (track.mode === "disabled") {
6814
+ track.mode = "hidden";
6815
+ }
6816
+ const trackElementForTrack = this.findTrackElement(track);
6817
+ const actualSrc = trackElementForTrack ? trackElementForTrack.getAttribute("src") : null;
6818
+ const expectedTrackInfo = swappedTracks.find((t) => {
6819
+ const tEl = t.trackElement;
6820
+ return tEl && (tEl.track === track || tEl.getAttribute("srclang") === track.language && tEl.getAttribute("kind") === track.kind);
6821
+ });
6822
+ const expectedSrc = expectedTrackInfo ? expectedTrackInfo.describedSrc : null;
6823
+ if (expectedSrc && actualSrc && actualSrc !== expectedSrc) {
6824
+ checkLoaded();
6825
+ return;
6826
+ }
6827
+ if (track.readyState >= 2 && track.cues && track.cues.length > 0) {
6828
+ checkLoaded();
6829
+ } else {
6830
+ if (track.mode === "disabled") {
6831
+ track.mode = "hidden";
6832
+ }
6833
+ const onTrackLoad = () => {
6834
+ this.setManagedTimeout(checkLoaded, 300);
6835
+ };
6836
+ if (track.readyState >= 2) {
6837
+ this.setManagedTimeout(() => {
6838
+ if (track.cues && track.cues.length > 0) {
6839
+ checkLoaded();
6840
+ } else {
6841
+ track.addEventListener("load", onTrackLoad, { once: true });
6842
+ }
6843
+ }, 100);
6844
+ } else {
6845
+ track.addEventListener("load", onTrackLoad, { once: true });
6846
+ track.addEventListener("error", () => {
6847
+ checkLoaded();
6848
+ }, { once: true });
6849
+ }
6850
+ }
6851
+ });
6852
+ };
6853
+ const waitForTracks = () => {
6854
+ this.setManagedTimeout(() => {
6855
+ if (this.element.readyState >= 1) {
6856
+ onMetadataLoaded();
6857
+ } else {
6858
+ this.element.addEventListener("loadedmetadata", onMetadataLoaded, { once: true });
6859
+ this.setManagedTimeout(onMetadataLoaded, 2e3);
6860
+ }
6861
+ }, 500);
6862
+ };
6863
+ waitForTracks();
6864
+ setTimeout(() => {
6865
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6866
+ this.transcriptManager.loadTranscriptData();
6867
+ }
6868
+ }, 5e3);
6869
+ } else {
6870
+ setTimeout(() => {
6871
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6872
+ this.transcriptManager.loadTranscriptData();
6873
+ }
6874
+ }, 800);
6875
+ }
6876
+ }
5930
6877
  this.state.audioDescriptionEnabled = true;
5931
6878
  this.emit("audiodescriptionenabled");
5932
6879
  }
@@ -5936,7 +6883,64 @@ var Player = class extends EventEmitter {
5936
6883
  }
5937
6884
  const currentTime = this.state.currentTime;
5938
6885
  const wasPlaying = this.state.playing;
5939
- this.element.src = this.originalSrc;
6886
+ if (this.audioDescriptionCaptionTracks.length > 0) {
6887
+ this.audioDescriptionCaptionTracks.forEach((trackInfo) => {
6888
+ if (trackInfo.trackElement && trackInfo.originalTrackSrc) {
6889
+ trackInfo.trackElement.setAttribute("src", trackInfo.originalTrackSrc);
6890
+ }
6891
+ });
6892
+ }
6893
+ const allSourceElements = this.sourceElements;
6894
+ const hasSourceElementsToSwap = allSourceElements.some((el) => el.getAttribute("data-orig-src"));
6895
+ if (hasSourceElementsToSwap) {
6896
+ const sourcesToRestore = [];
6897
+ allSourceElements.forEach((sourceEl) => {
6898
+ const origSrcAttr = sourceEl.getAttribute("data-orig-src");
6899
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
6900
+ if (origSrcAttr) {
6901
+ const type = sourceEl.getAttribute("type");
6902
+ sourcesToRestore.push({
6903
+ src: origSrcAttr,
6904
+ // Use original version
6905
+ type,
6906
+ origSrc: origSrcAttr,
6907
+ descSrc: descSrcAttr
6908
+ // Keep data-desc-src for future swaps
6909
+ });
6910
+ } else {
6911
+ const type = sourceEl.getAttribute("type");
6912
+ const src = sourceEl.getAttribute("src");
6913
+ sourcesToRestore.push({
6914
+ src,
6915
+ type,
6916
+ origSrc: null,
6917
+ descSrc: descSrcAttr
6918
+ });
6919
+ }
6920
+ });
6921
+ allSourceElements.forEach((sourceEl) => {
6922
+ sourceEl.remove();
6923
+ });
6924
+ sourcesToRestore.forEach((sourceInfo) => {
6925
+ const newSource = document.createElement("source");
6926
+ newSource.setAttribute("src", sourceInfo.src);
6927
+ if (sourceInfo.type) {
6928
+ newSource.setAttribute("type", sourceInfo.type);
6929
+ }
6930
+ if (sourceInfo.origSrc) {
6931
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
6932
+ }
6933
+ if (sourceInfo.descSrc) {
6934
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
6935
+ }
6936
+ this.element.appendChild(newSource);
6937
+ });
6938
+ this.element.load();
6939
+ } else {
6940
+ const originalSrcToUse = this.originalAudioDescriptionSource || this.originalSrc;
6941
+ this.element.src = originalSrcToUse;
6942
+ this.element.load();
6943
+ }
5940
6944
  await new Promise((resolve) => {
5941
6945
  const onLoadedMetadata = () => {
5942
6946
  this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
@@ -5948,13 +6952,50 @@ var Player = class extends EventEmitter {
5948
6952
  if (wasPlaying) {
5949
6953
  this.play();
5950
6954
  }
6955
+ if (this.transcriptManager && this.transcriptManager.isVisible) {
6956
+ this.setManagedTimeout(() => {
6957
+ if (this.transcriptManager && this.transcriptManager.loadTranscriptData) {
6958
+ this.transcriptManager.loadTranscriptData();
6959
+ }
6960
+ }, 500);
6961
+ }
5951
6962
  this.state.audioDescriptionEnabled = false;
5952
6963
  this.emit("audiodescriptiondisabled");
5953
6964
  }
5954
6965
  async toggleAudioDescription() {
5955
- const textTracks = Array.from(this.element.textTracks || []);
5956
- const descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
5957
- if (descriptionTrack) {
6966
+ const descriptionTrack = this.findTextTrack("descriptions");
6967
+ const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
6968
+ if (descriptionTrack && hasAudioDescriptionSrc) {
6969
+ if (this.state.audioDescriptionEnabled) {
6970
+ descriptionTrack.mode = "hidden";
6971
+ await this.disableAudioDescription();
6972
+ } else {
6973
+ await this.enableAudioDescription();
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
+ }
6997
+ }
6998
+ } else if (descriptionTrack) {
5958
6999
  if (descriptionTrack.mode === "showing") {
5959
7000
  descriptionTrack.mode = "hidden";
5960
7001
  this.state.audioDescriptionEnabled = false;
@@ -5964,7 +7005,7 @@ var Player = class extends EventEmitter {
5964
7005
  this.state.audioDescriptionEnabled = true;
5965
7006
  this.emit("audiodescriptionenabled");
5966
7007
  }
5967
- } else if (this.audioDescriptionSrc) {
7008
+ } else if (hasAudioDescriptionSrc) {
5968
7009
  if (this.state.audioDescriptionEnabled) {
5969
7010
  await this.disableAudioDescription();
5970
7011
  } else {
@@ -6365,9 +7406,25 @@ var Player = class extends EventEmitter {
6365
7406
  }
6366
7407
  }
6367
7408
  // Logging
6368
- log(message, type = "log") {
6369
- if (this.options.debug) {
6370
- 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);
6371
7428
  }
6372
7429
  }
6373
7430
  // Setup responsive handlers
@@ -6427,7 +7484,7 @@ var Player = class extends EventEmitter {
6427
7484
  this.controlBar.updateFullscreenButton();
6428
7485
  }
6429
7486
  if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== "none") {
6430
- setTimeout(() => {
7487
+ this.setManagedTimeout(() => {
6431
7488
  requestAnimationFrame(() => {
6432
7489
  this.storage.saveSignLanguagePreferences({ size: null });
6433
7490
  this.signLanguageDesiredPosition = "bottom-right";
@@ -6490,12 +7547,368 @@ var Player = class extends EventEmitter {
6490
7547
  document.removeEventListener("MSFullscreenChange", this.fullscreenChangeHandler);
6491
7548
  this.fullscreenChangeHandler = null;
6492
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
+ }
6493
7568
  if (this.container && this.container.parentNode) {
6494
7569
  this.container.parentNode.insertBefore(this.element, this.container);
6495
7570
  this.container.parentNode.removeChild(this.container);
6496
7571
  }
6497
7572
  this.removeAllListeners();
6498
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
+ }
6499
7912
  };
6500
7913
  Player.instances = [];
6501
7914