vidply 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +30 -2
  2. package/dist/dev/{vidply.AudioDescriptionManager-6RFVF27Z.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +6 -6
  3. package/dist/dev/{vidply.CaptionStyleMenu-R2SCQ6VP.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
  4. package/dist/dev/{vidply.DASHRenderer-EOVEW73H.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
  5. package/dist/dev/{vidply.FloatingPlayerManager-V5XQWS75.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
  6. package/dist/dev/{vidply.HLSRenderer-ZBEFVH5W.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
  7. package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
  8. package/dist/dev/{vidply.SignLanguageManager-7IAJOVF6.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
  9. package/dist/dev/{vidply.SoundCloudRenderer-OA7QQYGN.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
  10. package/dist/dev/{vidply.TranscriptManager-BPM3T23L.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
  11. package/dist/dev/{vidply.VimeoRenderer-BRHFYPNJ.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
  12. package/dist/dev/{vidply.YouTubeRenderer-HRTN4EJ5.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
  13. package/dist/dev/{vidply.chunk-UHMVDBDB.js → vidply.chunk-6LYZYI5G.js} +3 -3
  14. package/dist/dev/{vidply.chunk-W5BR65JY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
  15. package/dist/dev/{vidply.chunk-XAK3DPTO.js → vidply.chunk-CGYIED2I.js} +2 -2
  16. package/dist/dev/{vidply.chunk-YVCYQZJI.js → vidply.chunk-HZ7UOKOU.js} +3 -3
  17. package/dist/dev/{vidply.chunk-NSMAZCMB.js → vidply.chunk-I6EDRYGJ.js} +33 -6
  18. package/dist/dev/{vidply.chunk-NSMAZCMB.js.map → vidply.chunk-I6EDRYGJ.js.map} +2 -2
  19. package/dist/dev/{vidply.chunk-LDVFAFGC.js → vidply.chunk-JKMQGLCJ.js} +5 -5
  20. package/dist/dev/{vidply.chunk-HOYEMJSD.js → vidply.chunk-JZNZX32Z.js} +2 -2
  21. package/dist/dev/{vidply.chunk-JAKZNGOR.js → vidply.chunk-P25MRJ6O.js} +2 -2
  22. package/dist/dev/{vidply.chunk-5IK3IC5V.js → vidply.chunk-PRFNESYA.js} +2 -2
  23. package/dist/dev/{vidply.chunk-RQG3MA7W.js → vidply.chunk-UI3RYCVF.js} +4 -3
  24. package/dist/dev/{vidply.chunk-RQG3MA7W.js.map → vidply.chunk-UI3RYCVF.js.map} +2 -2
  25. package/dist/dev/{vidply.chunk-C55OBT5H.js → vidply.chunk-UNFGMABP.js} +2 -2
  26. package/dist/dev/{vidply.de-5W5LBFSJ.js → vidply.de-VAUVSQXC.js} +29 -2
  27. package/dist/dev/{vidply.de-5W5LBFSJ.js.map → vidply.de-VAUVSQXC.js.map} +2 -2
  28. package/dist/dev/{vidply.es-SHDAT6L6.js → vidply.es-G65HFWO6.js} +29 -2
  29. package/dist/dev/{vidply.es-SHDAT6L6.js.map → vidply.es-G65HFWO6.js.map} +2 -2
  30. package/dist/dev/vidply.esm.js +594 -20
  31. package/dist/dev/vidply.esm.js.map +3 -3
  32. package/dist/dev/{vidply.fr-2JANOMNB.js → vidply.fr-MJNOZELM.js} +29 -2
  33. package/dist/dev/{vidply.fr-2JANOMNB.js.map → vidply.fr-MJNOZELM.js.map} +2 -2
  34. package/dist/dev/{vidply.ja-WWMSTEJH.js → vidply.ja-INMDHIYJ.js} +29 -2
  35. package/dist/dev/{vidply.ja-WWMSTEJH.js.map → vidply.ja-INMDHIYJ.js.map} +2 -2
  36. package/dist/legacy/vidply.js +718 -3
  37. package/dist/legacy/vidply.js.map +3 -3
  38. package/dist/legacy/vidply.min.js +2 -2
  39. package/dist/legacy/vidply.min.meta.json +72 -19
  40. package/dist/prod/{vidply.AudioDescriptionManager-YTDVDQBO.min.js → vidply.AudioDescriptionManager-EKWUNOPE.min.js} +2 -2
  41. package/dist/prod/{vidply.CaptionStyleMenu-2AFF3HF2.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
  42. package/dist/prod/{vidply.DASHRenderer-URDOTOKS.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
  43. package/dist/prod/{vidply.FloatingPlayerManager-QUBFK7CQ.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
  44. package/dist/prod/{vidply.HLSRenderer-46KNUVAB.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
  45. package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
  46. package/dist/prod/{vidply.SignLanguageManager-H4C36EVL.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
  47. package/dist/prod/{vidply.SoundCloudRenderer-OYSWMW7D.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
  48. package/dist/prod/{vidply.TranscriptManager-BA3DAS63.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
  49. package/dist/prod/{vidply.VimeoRenderer-NF3COTHU.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
  50. package/dist/prod/{vidply.YouTubeRenderer-UCQKYIJ4.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
  51. package/dist/prod/{vidply.chunk-7S5AFTST.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
  52. package/dist/prod/{vidply.chunk-EUOA2DJK.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
  53. package/dist/prod/{vidply.chunk-JPMAK3NS.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
  54. package/dist/prod/{vidply.chunk-XXUH7ADU.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
  55. package/dist/prod/{vidply.chunk-M76ABMKB.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
  56. package/dist/prod/{vidply.chunk-G6KGXNEW.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
  57. package/dist/prod/{vidply.chunk-6ECBIYVI.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
  58. package/dist/prod/{vidply.chunk-2HNCP64N.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
  59. package/dist/prod/{vidply.chunk-WRVDVD33.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
  60. package/dist/prod/{vidply.chunk-L4O44K63.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
  61. package/dist/prod/{vidply.chunk-X2FQEQYR.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
  62. package/dist/prod/{vidply.de-JMCATTKZ.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
  63. package/dist/prod/{vidply.es-YVG37V7Z.min.js → vidply.es-BOISUV2S.min.js} +2 -2
  64. package/dist/prod/vidply.esm.min.js +2 -2
  65. package/dist/prod/{vidply.fr-OGKGN2R4.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
  66. package/dist/prod/{vidply.ja-Z6DS7QLS.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
  67. package/dist/vidply.css +56 -0
  68. package/dist/vidply.esm.min.meta.json +154 -112
  69. package/dist/vidply.min.css +1 -1
  70. package/package.json +7 -7
  71. package/src/controls/ControlBar.ts +32 -0
  72. package/src/controls/KeyboardHelp.ts +326 -0
  73. package/src/controls/KeyboardManager.ts +4 -0
  74. package/src/core/MediaSessionManager.ts +341 -0
  75. package/src/core/Player.ts +59 -1
  76. package/src/i18n/languages/de.ts +27 -0
  77. package/src/i18n/languages/en.ts +27 -0
  78. package/src/i18n/languages/es.ts +27 -0
  79. package/src/i18n/languages/fr.ts +27 -0
  80. package/src/i18n/languages/ja.ts +27 -0
  81. package/src/icons/Icons.ts +2 -1
  82. package/src/styles/vidply.css +56 -0
  83. package/src/types/events.ts +4 -0
  84. package/src/types/options.ts +14 -0
  85. package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +0 -12
  86. package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +0 -6
  87. /package/dist/dev/{vidply.AudioDescriptionManager-6RFVF27Z.js.map → vidply.AudioDescriptionManager-JYYLK5KB.js.map} +0 -0
  88. /package/dist/dev/{vidply.CaptionStyleMenu-R2SCQ6VP.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
  89. /package/dist/dev/{vidply.DASHRenderer-EOVEW73H.js.map → vidply.DASHRenderer-VEJ4HDHO.js.map} +0 -0
  90. /package/dist/dev/{vidply.FloatingPlayerManager-V5XQWS75.js.map → vidply.FloatingPlayerManager-C2UVRPZY.js.map} +0 -0
  91. /package/dist/dev/{vidply.HLSRenderer-ZBEFVH5W.js.map → vidply.HLSRenderer-ZVBK2TNQ.js.map} +0 -0
  92. /package/dist/dev/{vidply.HTML5Renderer-P7ICKYU4.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
  93. /package/dist/dev/{vidply.SignLanguageManager-7IAJOVF6.js.map → vidply.SignLanguageManager-FLUCBW5H.js.map} +0 -0
  94. /package/dist/dev/{vidply.SoundCloudRenderer-OA7QQYGN.js.map → vidply.SoundCloudRenderer-MGMAJ7NK.js.map} +0 -0
  95. /package/dist/dev/{vidply.TranscriptManager-BPM3T23L.js.map → vidply.TranscriptManager-Y2RYXHJW.js.map} +0 -0
  96. /package/dist/dev/{vidply.VimeoRenderer-BRHFYPNJ.js.map → vidply.VimeoRenderer-RQFQD7OE.js.map} +0 -0
  97. /package/dist/dev/{vidply.YouTubeRenderer-HRTN4EJ5.js.map → vidply.YouTubeRenderer-EU2BARZF.js.map} +0 -0
  98. /package/dist/dev/{vidply.chunk-UHMVDBDB.js.map → vidply.chunk-6LYZYI5G.js.map} +0 -0
  99. /package/dist/dev/{vidply.chunk-W5BR65JY.js.map → vidply.chunk-A4ESEOQ2.js.map} +0 -0
  100. /package/dist/dev/{vidply.chunk-XAK3DPTO.js.map → vidply.chunk-CGYIED2I.js.map} +0 -0
  101. /package/dist/dev/{vidply.chunk-YVCYQZJI.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
  102. /package/dist/dev/{vidply.chunk-LDVFAFGC.js.map → vidply.chunk-JKMQGLCJ.js.map} +0 -0
  103. /package/dist/dev/{vidply.chunk-HOYEMJSD.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
  104. /package/dist/dev/{vidply.chunk-JAKZNGOR.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
  105. /package/dist/dev/{vidply.chunk-5IK3IC5V.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
  106. /package/dist/dev/{vidply.chunk-C55OBT5H.js.map → vidply.chunk-UNFGMABP.js.map} +0 -0
@@ -1,39 +1,39 @@
1
1
  /*!
2
- * VidPly v1.2.1 - Universal, Accessible Video Player
2
+ * VidPly v1.2.3 - Universal, Accessible Video Player
3
3
  * (c) 2026 Matthias Peltzer
4
4
  * Released under GPL-2.0-or-later License
5
5
  */
6
6
  import {
7
7
  TimeUtils
8
- } from "./vidply.chunk-YVCYQZJI.js";
8
+ } from "./vidply.chunk-HZ7UOKOU.js";
9
9
  import {
10
10
  createIconElement,
11
11
  createPlayOverlay
12
- } from "./vidply.chunk-RQG3MA7W.js";
12
+ } from "./vidply.chunk-UI3RYCVF.js";
13
13
  import {
14
14
  focusElement
15
- } from "./vidply.chunk-JAKZNGOR.js";
15
+ } from "./vidply.chunk-P25MRJ6O.js";
16
16
  import {
17
17
  HTML5Renderer
18
- } from "./vidply.chunk-XAK3DPTO.js";
18
+ } from "./vidply.chunk-CGYIED2I.js";
19
19
  import {
20
20
  CaptionManager
21
- } from "./vidply.chunk-LDVFAFGC.js";
21
+ } from "./vidply.chunk-JKMQGLCJ.js";
22
22
  import {
23
23
  StorageManager
24
- } from "./vidply.chunk-UHMVDBDB.js";
24
+ } from "./vidply.chunk-6LYZYI5G.js";
25
25
  import {
26
26
  debounce,
27
27
  isMobile,
28
28
  rafWithTimeout,
29
29
  reducedMotionScrollOptions,
30
30
  throttle
31
- } from "./vidply.chunk-5IK3IC5V.js";
31
+ } from "./vidply.chunk-PRFNESYA.js";
32
32
  import {
33
33
  DOMUtils,
34
34
  i18n,
35
35
  isForbiddenKey
36
- } from "./vidply.chunk-NSMAZCMB.js";
36
+ } from "./vidply.chunk-I6EDRYGJ.js";
37
37
 
38
38
  // src/utils/EventEmitter.ts
39
39
  var EventEmitter = class {
@@ -960,6 +960,12 @@ var ControlBar = class {
960
960
  }
961
961
  this.rightButtons.appendChild(btn);
962
962
  }
963
+ if (this.player.options.helpButton && this.player.options.keyboard) {
964
+ const btn = this.createHelpButton();
965
+ btn.dataset.overflowPriority = "3";
966
+ btn.dataset.overflowPriorityMobile = "3";
967
+ this.rightButtons.appendChild(btn);
968
+ }
963
969
  this.overflowMenuButton = this.createOverflowMenuButton();
964
970
  this.overflowMenuButton.style.display = "none";
965
971
  this.rightButtons.appendChild(this.overflowMenuButton);
@@ -2051,7 +2057,7 @@ var ControlBar = class {
2051
2057
  return button;
2052
2058
  }
2053
2059
  showCaptionStyleMenu(button) {
2054
- import("./vidply.CaptionStyleMenu-R2SCQ6VP.js").then(({ showCaptionStyleMenu }) => showCaptionStyleMenu(this, button)).catch((error) => this.player.log("Failed to load caption style menu:", error, "error"));
2060
+ import("./vidply.CaptionStyleMenu-47RCKQF5.js").then(({ showCaptionStyleMenu }) => showCaptionStyleMenu(this, button)).catch((error) => this.player.log("Failed to load caption style menu:", error, "error"));
2055
2061
  }
2056
2062
  createSpeedButton() {
2057
2063
  const button = DOMUtils.createElement("button", {
@@ -2274,6 +2280,24 @@ var ControlBar = class {
2274
2280
  const isVisible = this.player.transcriptManager && this.player.transcriptManager.isVisible;
2275
2281
  this.controls.transcript.setAttribute("aria-expanded", isVisible ? "true" : "false");
2276
2282
  }
2283
+ createHelpButton() {
2284
+ const ariaLabel = i18n.t("help.button");
2285
+ const button = DOMUtils.createElement("button", {
2286
+ className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-help`,
2287
+ attributes: {
2288
+ "type": "button",
2289
+ "aria-label": ariaLabel,
2290
+ "aria-haspopup": "dialog"
2291
+ }
2292
+ });
2293
+ button.appendChild(createIconElement("help"));
2294
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
2295
+ button.addEventListener("click", () => {
2296
+ this.player.toggleKeyboardHelp();
2297
+ });
2298
+ this.controls.help = button;
2299
+ return button;
2300
+ }
2277
2301
  createAudioDescriptionButton() {
2278
2302
  const ariaLabel = i18n.t("player.audioDescription");
2279
2303
  const button = DOMUtils.createElement("button", {
@@ -3507,6 +3531,9 @@ var KeyboardManager = class {
3507
3531
  return true;
3508
3532
  }
3509
3533
  return false;
3534
+ case "help":
3535
+ this.player.toggleKeyboardHelp();
3536
+ return true;
3510
3537
  default:
3511
3538
  return false;
3512
3539
  }
@@ -3594,6 +3621,252 @@ var KeyboardManager = class {
3594
3621
  }
3595
3622
  };
3596
3623
 
3624
+ // src/core/MediaSessionManager.ts
3625
+ var POSITION_THROTTLE_MS = 1e3;
3626
+ var activeManager = null;
3627
+ var MediaSessionManager = class {
3628
+ player;
3629
+ supported;
3630
+ handlers = {};
3631
+ boundActions = [];
3632
+ lastPositionUpdate = 0;
3633
+ constructor(player) {
3634
+ this.player = player;
3635
+ this.supported = typeof navigator !== "undefined" && "mediaSession" in navigator;
3636
+ if (!this.supported) return;
3637
+ this.attachEvents();
3638
+ if (activeManager === null) {
3639
+ this.claimSession();
3640
+ }
3641
+ }
3642
+ /** Does this manager currently own the global media session? */
3643
+ isActive() {
3644
+ return activeManager === this;
3645
+ }
3646
+ /**
3647
+ * Take ownership of the global session: (re)register the action handlers so
3648
+ * the OS controls drive this player, and refresh metadata/state/position.
3649
+ */
3650
+ claimSession() {
3651
+ if (!this.supported) return;
3652
+ activeManager = this;
3653
+ this.setupActionHandlers();
3654
+ this.updateMetadata();
3655
+ this.updatePlaybackState();
3656
+ this.updatePositionState(true);
3657
+ }
3658
+ get session() {
3659
+ return navigator.mediaSession;
3660
+ }
3661
+ setActionHandler(action, handler) {
3662
+ try {
3663
+ this.session.setActionHandler(action, handler);
3664
+ if (handler && !this.boundActions.includes(action)) {
3665
+ this.boundActions.push(action);
3666
+ }
3667
+ } catch {
3668
+ }
3669
+ }
3670
+ setupActionHandlers() {
3671
+ this.setActionHandler("play", () => this.player.play());
3672
+ this.setActionHandler("pause", () => this.player.pause());
3673
+ this.setActionHandler("stop", () => this.player.stop());
3674
+ this.setActionHandler("seekbackward", (details) => {
3675
+ this.player.seekBackward(this.offsetFrom(details));
3676
+ });
3677
+ this.setActionHandler("seekforward", (details) => {
3678
+ this.player.seekForward(this.offsetFrom(details));
3679
+ });
3680
+ this.setActionHandler("seekto", (details) => {
3681
+ if (details && typeof details.seekTime === "number") {
3682
+ this.player.seek(details.seekTime);
3683
+ }
3684
+ });
3685
+ this.updateTrackHandlers();
3686
+ }
3687
+ offsetFrom(details) {
3688
+ const offset = details && typeof details.seekOffset === "number" ? details.seekOffset : void 0;
3689
+ return typeof offset === "number" && offset > 0 ? offset : void 0;
3690
+ }
3691
+ /**
3692
+ * previous/next track only make sense with a multi-item playlist; bind
3693
+ * or clear them whenever the playlist state changes so the OS shows the
3694
+ * correct affordances.
3695
+ */
3696
+ updateTrackHandlers() {
3697
+ const pm = this.player.playlistManager;
3698
+ const hasPlaylist = Boolean(pm && Array.isArray(pm.tracks) && pm.tracks.length > 1);
3699
+ if (hasPlaylist && pm) {
3700
+ this.setActionHandler("previoustrack", () => pm.previous());
3701
+ this.setActionHandler("nexttrack", () => pm.next());
3702
+ } else {
3703
+ this.setActionHandler("previoustrack", null);
3704
+ this.setActionHandler("nexttrack", null);
3705
+ }
3706
+ }
3707
+ attachEvents() {
3708
+ this.handlers = {
3709
+ // Starting playback makes this player the session owner, taking over
3710
+ // the OS controls from any other player on the page.
3711
+ play: () => this.claimSession(),
3712
+ pause: () => {
3713
+ if (!this.isActive()) return;
3714
+ this.updatePlaybackState();
3715
+ this.updatePositionState(true);
3716
+ },
3717
+ ended: () => {
3718
+ if (!this.isActive()) return;
3719
+ this.updatePlaybackState();
3720
+ },
3721
+ timeupdate: () => {
3722
+ if (!this.isActive()) return;
3723
+ this.updatePositionState();
3724
+ },
3725
+ durationchange: () => {
3726
+ if (!this.isActive()) return;
3727
+ this.updatePositionState(true);
3728
+ },
3729
+ ratechange: () => {
3730
+ if (!this.isActive()) return;
3731
+ this.updatePositionState(true);
3732
+ },
3733
+ loadedmetadata: () => {
3734
+ if (!this.isActive()) return;
3735
+ this.updateMetadata();
3736
+ this.updatePositionState(true);
3737
+ },
3738
+ playlisttrackchange: () => {
3739
+ if (!this.isActive()) return;
3740
+ this.updateMetadata();
3741
+ this.updateTrackHandlers();
3742
+ this.updatePositionState(true);
3743
+ }
3744
+ };
3745
+ for (const [event, handler] of Object.entries(this.handlers)) {
3746
+ if (handler) {
3747
+ this.player.on(event, handler);
3748
+ }
3749
+ }
3750
+ }
3751
+ resolveMetadata() {
3752
+ const opts = this.player.options;
3753
+ let title = opts.title || "";
3754
+ let artist = opts.artist || "";
3755
+ const album = opts.album || "";
3756
+ let poster = opts.poster || null;
3757
+ const pm = this.player.playlistManager;
3758
+ if (pm && Array.isArray(pm.tracks) && pm.currentIndex >= 0) {
3759
+ const track = pm.tracks[pm.currentIndex];
3760
+ if (track) {
3761
+ if (track.title) title = track.title;
3762
+ if (track.artist) artist = track.artist;
3763
+ if (track.poster) poster = track.poster;
3764
+ }
3765
+ }
3766
+ if (!title && typeof document !== "undefined") {
3767
+ title = document.title || "VidPly";
3768
+ }
3769
+ return { title, artist, album, poster };
3770
+ }
3771
+ updateMetadata() {
3772
+ if (!this.supported || typeof window === "undefined" || typeof window.MediaMetadata === "undefined") {
3773
+ return;
3774
+ }
3775
+ const { title, artist, album, poster } = this.resolveMetadata();
3776
+ const artwork = [];
3777
+ if (poster) {
3778
+ try {
3779
+ const src = this.player.resolvePosterPath(poster);
3780
+ if (src) {
3781
+ artwork.push({ src });
3782
+ }
3783
+ } catch {
3784
+ }
3785
+ }
3786
+ try {
3787
+ this.session.metadata = new window.MediaMetadata({
3788
+ title,
3789
+ artist,
3790
+ album,
3791
+ artwork
3792
+ });
3793
+ } catch {
3794
+ }
3795
+ }
3796
+ updatePlaybackState() {
3797
+ if (!this.supported) return;
3798
+ try {
3799
+ this.session.playbackState = this.player.state.playing ? "playing" : "paused";
3800
+ } catch {
3801
+ }
3802
+ }
3803
+ /**
3804
+ * Push the current position to the OS scrubber. `timeupdate` fires
3805
+ * several times a second, so non-forced updates are throttled.
3806
+ */
3807
+ updatePositionState(force = false) {
3808
+ if (!this.supported || typeof this.session.setPositionState !== "function") {
3809
+ return;
3810
+ }
3811
+ const now = Date.now();
3812
+ if (!force && now - this.lastPositionUpdate < POSITION_THROTTLE_MS) {
3813
+ return;
3814
+ }
3815
+ this.lastPositionUpdate = now;
3816
+ const duration = this.player.state.duration;
3817
+ if (!Number.isFinite(duration) || duration <= 0) {
3818
+ try {
3819
+ this.session.setPositionState();
3820
+ } catch {
3821
+ }
3822
+ return;
3823
+ }
3824
+ const playbackRate = this.player.state.playbackSpeed || 1;
3825
+ const position = Math.min(Math.max(0, this.player.state.currentTime || 0), duration);
3826
+ try {
3827
+ this.session.setPositionState({
3828
+ duration,
3829
+ playbackRate: playbackRate > 0 ? playbackRate : 1,
3830
+ position
3831
+ });
3832
+ } catch {
3833
+ }
3834
+ }
3835
+ destroy() {
3836
+ if (!this.supported) return;
3837
+ for (const [event, handler] of Object.entries(this.handlers)) {
3838
+ if (handler) {
3839
+ this.player.off(event, handler);
3840
+ }
3841
+ }
3842
+ this.handlers = {};
3843
+ if (this.isActive()) {
3844
+ for (const action of this.boundActions) {
3845
+ try {
3846
+ this.session.setActionHandler(action, null);
3847
+ } catch {
3848
+ }
3849
+ }
3850
+ try {
3851
+ this.session.metadata = null;
3852
+ } catch {
3853
+ }
3854
+ try {
3855
+ if (typeof this.session.setPositionState === "function") {
3856
+ this.session.setPositionState();
3857
+ }
3858
+ } catch {
3859
+ }
3860
+ try {
3861
+ this.session.playbackState = "none";
3862
+ } catch {
3863
+ }
3864
+ activeManager = null;
3865
+ }
3866
+ this.boundActions = [];
3867
+ }
3868
+ };
3869
+
3597
3870
  // src/utils/UrlSafe.ts
3598
3871
  function sanitizePosterUrl(input) {
3599
3872
  if (typeof input !== "string" || input.length === 0 || input.length > 4096) {
@@ -4694,27 +4967,280 @@ var MetadataAlertsManager = class {
4694
4967
  }
4695
4968
  };
4696
4969
 
4970
+ // src/controls/KeyboardHelp.ts
4971
+ var ACTION_ORDER = [
4972
+ "play-pause",
4973
+ "seek-backward",
4974
+ "seek-forward",
4975
+ "volume-up",
4976
+ "volume-down",
4977
+ "mute",
4978
+ "captions",
4979
+ "caption-style-menu",
4980
+ "speed-down",
4981
+ "speed-up",
4982
+ "speed-menu",
4983
+ "quality-menu",
4984
+ "chapters-menu",
4985
+ "transcript-toggle",
4986
+ "fullscreen",
4987
+ "help"
4988
+ ];
4989
+ var ACTION_REQUIRES_CONTROL = {
4990
+ captions: "captions",
4991
+ "caption-style-menu": "captionStyle",
4992
+ "speed-down": "speed",
4993
+ "speed-up": "speed",
4994
+ "speed-menu": "speed",
4995
+ "quality-menu": "quality",
4996
+ "chapters-menu": "chapters",
4997
+ "transcript-toggle": "transcript",
4998
+ fullscreen: "fullscreen"
4999
+ };
5000
+ var KeyboardHelp = class {
5001
+ player;
5002
+ isOpen = false;
5003
+ overlay = null;
5004
+ _triggerElement = null;
5005
+ _keydownHandler = null;
5006
+ _content = null;
5007
+ constructor(player) {
5008
+ this.player = player;
5009
+ }
5010
+ get prefix() {
5011
+ return this.player.options.classPrefix;
5012
+ }
5013
+ /**
5014
+ * Turn a raw KeyboardEvent.key value into a human-readable label. Arrow
5015
+ * keys become universally understood glyphs; the space bar and single
5016
+ * letters are normalised for legibility.
5017
+ */
5018
+ formatKey(key) {
5019
+ switch (key) {
5020
+ case " ":
5021
+ return i18n.t("help.keys.space");
5022
+ case "ArrowUp":
5023
+ return "↑";
5024
+ case "ArrowDown":
5025
+ return "↓";
5026
+ case "ArrowLeft":
5027
+ return "←";
5028
+ case "ArrowRight":
5029
+ return "→";
5030
+ case "Escape":
5031
+ return "Esc";
5032
+ default:
5033
+ return key.length === 1 ? key.toUpperCase() : key;
5034
+ }
5035
+ }
5036
+ createElement() {
5037
+ const titleId = `${this.prefix}-help-title-${this.player.instanceId}`;
5038
+ const overlay = DOMUtils.createElement("div", {
5039
+ className: `${this.prefix}-settings-overlay ${this.prefix}-help-overlay`,
5040
+ attributes: {
5041
+ role: "dialog",
5042
+ "aria-modal": "true",
5043
+ "aria-labelledby": titleId
5044
+ }
5045
+ });
5046
+ overlay.style.display = "none";
5047
+ const dialog = DOMUtils.createElement("div", {
5048
+ className: `${this.prefix}-settings-dialog ${this.prefix}-help-dialog`
5049
+ });
5050
+ const header = DOMUtils.createElement("div", {
5051
+ className: `${this.prefix}-settings-header`
5052
+ });
5053
+ const title = DOMUtils.createElement("h2", {
5054
+ textContent: i18n.t("help.title"),
5055
+ attributes: { id: titleId }
5056
+ });
5057
+ const closeButton = DOMUtils.createElement("button", {
5058
+ className: `${this.prefix}-button ${this.prefix}-settings-close`,
5059
+ attributes: {
5060
+ type: "button",
5061
+ "aria-label": i18n.t("help.close")
5062
+ }
5063
+ });
5064
+ closeButton.appendChild(createIconElement("close"));
5065
+ closeButton.addEventListener("click", () => this.hide());
5066
+ header.appendChild(title);
5067
+ header.appendChild(closeButton);
5068
+ const content = DOMUtils.createElement("div", {
5069
+ className: `${this.prefix}-settings-content`
5070
+ });
5071
+ this._content = content;
5072
+ content.appendChild(this.buildShortcutList());
5073
+ dialog.appendChild(header);
5074
+ dialog.appendChild(content);
5075
+ overlay.appendChild(dialog);
5076
+ overlay.addEventListener("click", (e) => {
5077
+ if (e.target === overlay) {
5078
+ this.hide();
5079
+ }
5080
+ });
5081
+ this._keydownHandler = (e) => {
5082
+ if (!this.isOpen || !this.overlay) return;
5083
+ if (e.key === "Escape") {
5084
+ e.preventDefault();
5085
+ this.hide();
5086
+ return;
5087
+ }
5088
+ if (e.key === "Tab") {
5089
+ const focusable = Array.from(
5090
+ this.overlay.querySelectorAll(
5091
+ 'button, [href], select, input, textarea, [tabindex]:not([tabindex="-1"])'
5092
+ )
5093
+ ).filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
5094
+ if (focusable.length === 0) return;
5095
+ const first = focusable[0];
5096
+ const last = focusable[focusable.length - 1];
5097
+ if (!first || !last) return;
5098
+ const active = document.activeElement;
5099
+ if (e.shiftKey && (active === first || !this.overlay.contains(active))) {
5100
+ e.preventDefault();
5101
+ last.focus({ preventScroll: true });
5102
+ } else if (!e.shiftKey && active === last) {
5103
+ e.preventDefault();
5104
+ first.focus({ preventScroll: true });
5105
+ }
5106
+ }
5107
+ };
5108
+ const lifecycleSignal = this.player.lifecycleSignal;
5109
+ document.addEventListener(
5110
+ "keydown",
5111
+ this._keydownHandler,
5112
+ lifecycleSignal ? { signal: lifecycleSignal } : void 0
5113
+ );
5114
+ return overlay;
5115
+ }
5116
+ /**
5117
+ * Whether a shortcut row is worth showing for *this* player. Feature actions
5118
+ * are hidden when their control isn't present (e.g. no captions track, an
5119
+ * audio-only player with no fullscreen). Core actions are always relevant.
5120
+ *
5121
+ * When the player has no control bar we can't infer availability, so nothing
5122
+ * is hidden — the shortcuts still work and we'd rather over-show than mislead.
5123
+ */
5124
+ isActionRelevant(action) {
5125
+ const requiredControl = ACTION_REQUIRES_CONTROL[action];
5126
+ if (!requiredControl) return true;
5127
+ const controlBar = this.player.controlBar;
5128
+ if (!controlBar || !controlBar.controls) return true;
5129
+ return Boolean(controlBar.controls[requiredControl]);
5130
+ }
5131
+ buildShortcutList() {
5132
+ const list = DOMUtils.createElement("dl", {
5133
+ className: `${this.prefix}-help-list`
5134
+ });
5135
+ const shortcuts = this.player.options.keyboardShortcuts;
5136
+ for (const action of ACTION_ORDER) {
5137
+ const keys = shortcuts[action];
5138
+ if (!Array.isArray(keys) || keys.length === 0) continue;
5139
+ if (!this.isActionRelevant(action)) continue;
5140
+ const term = DOMUtils.createElement("dt", {
5141
+ className: `${this.prefix}-help-action`,
5142
+ textContent: i18n.t(`help.actions.${action}`)
5143
+ });
5144
+ const desc = DOMUtils.createElement("dd", {
5145
+ className: `${this.prefix}-help-keys`
5146
+ });
5147
+ keys.forEach((key, index) => {
5148
+ if (index > 0) {
5149
+ desc.appendChild(
5150
+ DOMUtils.createElement("span", {
5151
+ className: `${this.prefix}-help-key-sep`,
5152
+ textContent: i18n.t("help.or")
5153
+ })
5154
+ );
5155
+ }
5156
+ desc.appendChild(
5157
+ DOMUtils.createElement("kbd", {
5158
+ className: `${this.prefix}-help-key`,
5159
+ textContent: this.formatKey(key)
5160
+ })
5161
+ );
5162
+ });
5163
+ list.appendChild(term);
5164
+ list.appendChild(desc);
5165
+ }
5166
+ return list;
5167
+ }
5168
+ show() {
5169
+ if (this.isOpen) return;
5170
+ if (!this.overlay) {
5171
+ this.overlay = this.createElement();
5172
+ this.player.container.appendChild(this.overlay);
5173
+ } else if (this._content) {
5174
+ this._content.replaceChildren(this.buildShortcutList());
5175
+ }
5176
+ const active = typeof document !== "undefined" ? document.activeElement : null;
5177
+ this._triggerElement = active && typeof active.focus === "function" ? active : null;
5178
+ this.overlay.style.display = "flex";
5179
+ this.isOpen = true;
5180
+ const closeButton = this.overlay.querySelector(`.${this.prefix}-settings-close`);
5181
+ closeButton?.focus({ preventScroll: true });
5182
+ this.player.emit("keyboardhelpopen");
5183
+ }
5184
+ hide() {
5185
+ if (!this.overlay) return;
5186
+ this.overlay.style.display = "none";
5187
+ this.isOpen = false;
5188
+ const trigger = this._triggerElement;
5189
+ this._triggerElement = null;
5190
+ if (trigger && document.contains(trigger)) {
5191
+ try {
5192
+ trigger.focus({ preventScroll: true });
5193
+ } catch {
5194
+ this.player.container?.focus();
5195
+ }
5196
+ } else {
5197
+ this.player.container?.focus();
5198
+ }
5199
+ this.player.emit("keyboardhelpclose");
5200
+ }
5201
+ toggle() {
5202
+ if (this.isOpen) {
5203
+ this.hide();
5204
+ } else {
5205
+ this.show();
5206
+ }
5207
+ }
5208
+ destroy() {
5209
+ if (this._keydownHandler) {
5210
+ document.removeEventListener("keydown", this._keydownHandler);
5211
+ this._keydownHandler = null;
5212
+ }
5213
+ if (this.overlay && this.overlay.parentNode) {
5214
+ this.overlay.parentNode.removeChild(this.overlay);
5215
+ }
5216
+ this.overlay = null;
5217
+ this._content = null;
5218
+ this._triggerElement = null;
5219
+ this.isOpen = false;
5220
+ }
5221
+ };
5222
+
4697
5223
  // src/core/Player.ts
4698
5224
  var AudioDescriptionManagerModule = null;
4699
5225
  var SignLanguageManagerModule = null;
4700
5226
  var FloatingPlayerManagerModule = null;
4701
5227
  async function loadAudioDescriptionManager() {
4702
5228
  if (!AudioDescriptionManagerModule) {
4703
- const module = await import("./vidply.AudioDescriptionManager-6RFVF27Z.js");
5229
+ const module = await import("./vidply.AudioDescriptionManager-JYYLK5KB.js");
4704
5230
  AudioDescriptionManagerModule = module.AudioDescriptionManager;
4705
5231
  }
4706
5232
  return AudioDescriptionManagerModule;
4707
5233
  }
4708
5234
  async function loadSignLanguageManager() {
4709
5235
  if (!SignLanguageManagerModule) {
4710
- const module = await import("./vidply.SignLanguageManager-7IAJOVF6.js");
5236
+ const module = await import("./vidply.SignLanguageManager-FLUCBW5H.js");
4711
5237
  SignLanguageManagerModule = module.SignLanguageManager;
4712
5238
  }
4713
5239
  return SignLanguageManagerModule;
4714
5240
  }
4715
5241
  async function loadFloatingPlayerManager() {
4716
5242
  if (!FloatingPlayerManagerModule) {
4717
- const module = await import("./vidply.FloatingPlayerManager-V5XQWS75.js");
5243
+ const module = await import("./vidply.FloatingPlayerManager-C2UVRPZY.js");
4718
5244
  FloatingPlayerManagerModule = module.FloatingPlayerManager;
4719
5245
  }
4720
5246
  return FloatingPlayerManagerModule;
@@ -4777,9 +5303,11 @@ var Player = class _Player extends EventEmitter {
4777
5303
  controlBar = null;
4778
5304
  captionManager = null;
4779
5305
  keyboardManager = null;
5306
+ mediaSessionManager = null;
4780
5307
  transcriptManager = null;
4781
5308
  playlistManager = null;
4782
5309
  settingsDialog = null;
5310
+ keyboardHelp = null;
4783
5311
  audioDescriptionManager = null;
4784
5312
  signLanguageManager = null;
4785
5313
  floatingPlayerManager = null;
@@ -4895,6 +5423,11 @@ var Player = class _Player extends EventEmitter {
4895
5423
  poster: null,
4896
5424
  responsive: true,
4897
5425
  fillContainer: false,
5426
+ // Media metadata + OS media controls (Media Session API)
5427
+ title: null,
5428
+ artist: null,
5429
+ album: null,
5430
+ mediaSession: true,
4898
5431
  // Playback
4899
5432
  autoplay: false,
4900
5433
  loop: false,
@@ -4942,6 +5475,7 @@ var Player = class _Player extends EventEmitter {
4942
5475
  captionsButton: true,
4943
5476
  transcriptButton: true,
4944
5477
  fullscreenButton: true,
5478
+ helpButton: true,
4945
5479
  pipButton: false,
4946
5480
  floating: false,
4947
5481
  floatingPosition: "bottom-right",
@@ -5000,7 +5534,8 @@ var Player = class _Player extends EventEmitter {
5000
5534
  "speed-menu": ["s"],
5001
5535
  "quality-menu": ["q"],
5002
5536
  "chapters-menu": ["j"],
5003
- "transcript-toggle": ["t"]
5537
+ "transcript-toggle": ["t"],
5538
+ "help": ["?"]
5004
5539
  },
5005
5540
  // Accessibility
5006
5541
  ariaLabels: {},
@@ -5323,6 +5858,9 @@ var Player = class _Player extends EventEmitter {
5323
5858
  if (this.options.keyboard) {
5324
5859
  this.keyboardManager = new KeyboardManager(this);
5325
5860
  }
5861
+ if (this.options.mediaSession) {
5862
+ this.mediaSessionManager = new MediaSessionManager(this);
5863
+ }
5326
5864
  this.setupResponsiveHandlers();
5327
5865
  if (this.options.startTime > 0) {
5328
5866
  this.seek(this.options.startTime);
@@ -5367,7 +5905,7 @@ var Player = class _Player extends EventEmitter {
5367
5905
  if (!this.options.transcript && !this.options.transcriptButton) {
5368
5906
  return null;
5369
5907
  }
5370
- const module = await import("./vidply.TranscriptManager-BPM3T23L.js");
5908
+ const module = await import("./vidply.TranscriptManager-Y2RYXHJW.js");
5371
5909
  const fallbackDefault = module.default;
5372
5910
  const Manager = module.TranscriptManager || fallbackDefault;
5373
5911
  if (!Manager) {
@@ -5836,19 +6374,19 @@ var Player = class _Player extends EventEmitter {
5836
6374
  }
5837
6375
  async _detectRendererClass(src) {
5838
6376
  if (src.includes("youtube.com") || src.includes("youtu.be")) {
5839
- const module = await import("./vidply.YouTubeRenderer-HRTN4EJ5.js");
6377
+ const module = await import("./vidply.YouTubeRenderer-EU2BARZF.js");
5840
6378
  return module.YouTubeRenderer ?? module.default;
5841
6379
  } else if (src.includes("vimeo.com")) {
5842
- const module = await import("./vidply.VimeoRenderer-BRHFYPNJ.js");
6380
+ const module = await import("./vidply.VimeoRenderer-RQFQD7OE.js");
5843
6381
  return module.VimeoRenderer ?? module.default;
5844
6382
  } else if (src.includes(".m3u8")) {
5845
- const module = await import("./vidply.HLSRenderer-ZBEFVH5W.js");
6383
+ const module = await import("./vidply.HLSRenderer-ZVBK2TNQ.js");
5846
6384
  return module.HLSRenderer ?? module.default;
5847
6385
  } else if (src.includes(".mpd")) {
5848
- const module = await import("./vidply.DASHRenderer-EOVEW73H.js");
6386
+ const module = await import("./vidply.DASHRenderer-VEJ4HDHO.js");
5849
6387
  return module.DASHRenderer ?? module.default;
5850
6388
  } else if (src.includes("soundcloud.com") || src.includes("api.soundcloud.com")) {
5851
- const module = await import("./vidply.SoundCloudRenderer-OA7QQYGN.js");
6389
+ const module = await import("./vidply.SoundCloudRenderer-MGMAJ7NK.js");
5852
6390
  return module.SoundCloudRenderer ?? module.default;
5853
6391
  }
5854
6392
  return HTML5Renderer;
@@ -6640,6 +7178,26 @@ var Player = class _Player extends EventEmitter {
6640
7178
  }
6641
7179
  hideSettings() {
6642
7180
  }
7181
+ /**
7182
+ * Lazily build (on first use) and toggle the keyboard-shortcuts help
7183
+ * dialog. Reflects the live `keyboardShortcuts` bindings, including any
7184
+ * consumer overrides.
7185
+ */
7186
+ toggleKeyboardHelp() {
7187
+ if (!this.keyboardHelp) {
7188
+ this.keyboardHelp = new KeyboardHelp(this);
7189
+ }
7190
+ this.keyboardHelp.toggle();
7191
+ }
7192
+ showKeyboardHelp() {
7193
+ if (!this.keyboardHelp) {
7194
+ this.keyboardHelp = new KeyboardHelp(this);
7195
+ }
7196
+ this.keyboardHelp.show();
7197
+ }
7198
+ hideKeyboardHelp() {
7199
+ this.keyboardHelp?.hide();
7200
+ }
6643
7201
  // Utility methods
6644
7202
  getCurrentTime() {
6645
7203
  return this.state.currentTime;
@@ -6779,6 +7337,22 @@ var Player = class _Player extends EventEmitter {
6779
7337
  }
6780
7338
  this.settingsDialog = null;
6781
7339
  }
7340
+ if (this.keyboardHelp && typeof this.keyboardHelp.destroy === "function") {
7341
+ try {
7342
+ this.keyboardHelp.destroy();
7343
+ } catch (err) {
7344
+ this.log(`KeyboardHelp.destroy failed: ${err}`, "warn");
7345
+ }
7346
+ this.keyboardHelp = null;
7347
+ }
7348
+ if (this.mediaSessionManager && typeof this.mediaSessionManager.destroy === "function") {
7349
+ try {
7350
+ this.mediaSessionManager.destroy();
7351
+ } catch (err) {
7352
+ this.log(`MediaSessionManager.destroy failed: ${err}`, "warn");
7353
+ }
7354
+ this.mediaSessionManager = null;
7355
+ }
6782
7356
  if (this.floatingPlayerManager) {
6783
7357
  try {
6784
7358
  this.floatingPlayerManager.destroy();