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,5 +1,5 @@
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
  */
@@ -428,6 +428,33 @@
428
428
  reset: "Reset to defaults",
429
429
  close: "Close"
430
430
  },
431
+ help: {
432
+ title: "Keyboard shortcuts",
433
+ button: "Keyboard shortcuts",
434
+ close: "Close",
435
+ or: "or",
436
+ keys: {
437
+ space: "Space"
438
+ },
439
+ actions: {
440
+ "play-pause": "Play / Pause",
441
+ "seek-backward": "Seek backward",
442
+ "seek-forward": "Seek forward",
443
+ "volume-up": "Volume up",
444
+ "volume-down": "Volume down",
445
+ "mute": "Mute / Unmute",
446
+ "captions": "Toggle captions",
447
+ "caption-style-menu": "Caption styling",
448
+ "speed-down": "Decrease speed",
449
+ "speed-up": "Increase speed",
450
+ "speed-menu": "Playback speed",
451
+ "quality-menu": "Quality",
452
+ "chapters-menu": "Chapters",
453
+ "transcript-toggle": "Toggle transcript",
454
+ "fullscreen": "Toggle fullscreen",
455
+ "help": "Keyboard shortcuts"
456
+ }
457
+ },
431
458
  speeds: {
432
459
  normal: "Normal"
433
460
  },
@@ -652,6 +679,33 @@
652
679
  reset: "Zurücksetzen",
653
680
  close: "Schließen"
654
681
  },
682
+ help: {
683
+ title: "Tastaturkürzel",
684
+ button: "Tastaturkürzel",
685
+ close: "Schließen",
686
+ or: "oder",
687
+ keys: {
688
+ space: "Leertaste"
689
+ },
690
+ actions: {
691
+ "play-pause": "Wiedergabe / Pause",
692
+ "seek-backward": "Zurückspulen",
693
+ "seek-forward": "Vorspulen",
694
+ "volume-up": "Lauter",
695
+ "volume-down": "Leiser",
696
+ "mute": "Stumm / Ton an",
697
+ "captions": "Untertitel umschalten",
698
+ "caption-style-menu": "Untertitel-Stil",
699
+ "speed-down": "Langsamer",
700
+ "speed-up": "Schneller",
701
+ "speed-menu": "Wiedergabegeschwindigkeit",
702
+ "quality-menu": "Qualität",
703
+ "chapters-menu": "Kapitel",
704
+ "transcript-toggle": "Transkript umschalten",
705
+ "fullscreen": "Vollbild umschalten",
706
+ "help": "Tastaturkürzel"
707
+ }
708
+ },
655
709
  speeds: {
656
710
  normal: "Normal"
657
711
  },
@@ -873,6 +927,33 @@
873
927
  reset: "Restablecer",
874
928
  close: "Cerrar"
875
929
  },
930
+ help: {
931
+ title: "Atajos de teclado",
932
+ button: "Atajos de teclado",
933
+ close: "Cerrar",
934
+ or: "o",
935
+ keys: {
936
+ space: "Espacio"
937
+ },
938
+ actions: {
939
+ "play-pause": "Reproducir / Pausar",
940
+ "seek-backward": "Retroceder",
941
+ "seek-forward": "Avanzar",
942
+ "volume-up": "Subir volumen",
943
+ "volume-down": "Bajar volumen",
944
+ "mute": "Silenciar / Activar sonido",
945
+ "captions": "Alternar subtítulos",
946
+ "caption-style-menu": "Estilo de subtítulos",
947
+ "speed-down": "Reducir velocidad",
948
+ "speed-up": "Aumentar velocidad",
949
+ "speed-menu": "Velocidad de reproducción",
950
+ "quality-menu": "Calidad",
951
+ "chapters-menu": "Capítulos",
952
+ "transcript-toggle": "Alternar transcripción",
953
+ "fullscreen": "Alternar pantalla completa",
954
+ "help": "Atajos de teclado"
955
+ }
956
+ },
876
957
  speeds: {
877
958
  normal: "Normal"
878
959
  },
@@ -1094,6 +1175,33 @@
1094
1175
  reset: "Réinitialiser",
1095
1176
  close: "Fermer"
1096
1177
  },
1178
+ help: {
1179
+ title: "Raccourcis clavier",
1180
+ button: "Raccourcis clavier",
1181
+ close: "Fermer",
1182
+ or: "ou",
1183
+ keys: {
1184
+ space: "Espace"
1185
+ },
1186
+ actions: {
1187
+ "play-pause": "Lecture / Pause",
1188
+ "seek-backward": "Reculer",
1189
+ "seek-forward": "Avancer",
1190
+ "volume-up": "Augmenter le volume",
1191
+ "volume-down": "Baisser le volume",
1192
+ "mute": "Couper / Activer le son",
1193
+ "captions": "Activer/désactiver les sous-titres",
1194
+ "caption-style-menu": "Style des sous-titres",
1195
+ "speed-down": "Ralentir",
1196
+ "speed-up": "Accélérer",
1197
+ "speed-menu": "Vitesse de lecture",
1198
+ "quality-menu": "Qualité",
1199
+ "chapters-menu": "Chapitres",
1200
+ "transcript-toggle": "Activer/désactiver la transcription",
1201
+ "fullscreen": "Activer/désactiver le plein écran",
1202
+ "help": "Raccourcis clavier"
1203
+ }
1204
+ },
1097
1205
  speeds: {
1098
1206
  normal: "Normal"
1099
1207
  },
@@ -1315,6 +1423,33 @@
1315
1423
  reset: "リセット",
1316
1424
  close: "閉じる"
1317
1425
  },
1426
+ help: {
1427
+ title: "キーボードショートカット",
1428
+ button: "キーボードショートカット",
1429
+ close: "閉じる",
1430
+ or: "または",
1431
+ keys: {
1432
+ space: "スペース"
1433
+ },
1434
+ actions: {
1435
+ "play-pause": "再生 / 一時停止",
1436
+ "seek-backward": "巻き戻し",
1437
+ "seek-forward": "早送り",
1438
+ "volume-up": "音量を上げる",
1439
+ "volume-down": "音量を下げる",
1440
+ "mute": "ミュート / ミュート解除",
1441
+ "captions": "字幕の切り替え",
1442
+ "caption-style-menu": "字幕スタイル",
1443
+ "speed-down": "速度を下げる",
1444
+ "speed-up": "速度を上げる",
1445
+ "speed-menu": "再生速度",
1446
+ "quality-menu": "画質",
1447
+ "chapters-menu": "チャプター",
1448
+ "transcript-toggle": "文字起こしの切り替え",
1449
+ "fullscreen": "全画面表示の切り替え",
1450
+ "help": "キーボードショートカット"
1451
+ }
1452
+ },
1318
1453
  speeds: {
1319
1454
  normal: "通常"
1320
1455
  },
@@ -1767,7 +1902,8 @@
1767
1902
  move: `<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>`,
1768
1903
  resize: `<path d="M21.71 11.29l-9-9c-.39-.39-1.02-.39-1.41 0l-9 9c-.39.39-.39 1.02 0 1.41l9 9c.39.39 1.02.39 1.41 0l9-9c.39-.38.39-1.01 0-1.41zM14 14.5V12h-4v2.5L7 11l3-3.5V10h4V7.5l3 3.5-3 3.5z"/>`,
1769
1904
  clock: `<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 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>`,
1770
- download: `<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>`
1905
+ download: `<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>`,
1906
+ help: `<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>`
1771
1907
  };
1772
1908
  svgWrapper = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">${paths}</svg>`;
1773
1909
  Icons = Object.fromEntries(
@@ -12376,6 +12512,12 @@
12376
12512
  }
12377
12513
  this.rightButtons.appendChild(btn);
12378
12514
  }
12515
+ if (this.player.options.helpButton && this.player.options.keyboard) {
12516
+ const btn = this.createHelpButton();
12517
+ btn.dataset.overflowPriority = "3";
12518
+ btn.dataset.overflowPriorityMobile = "3";
12519
+ this.rightButtons.appendChild(btn);
12520
+ }
12379
12521
  this.overflowMenuButton = this.createOverflowMenuButton();
12380
12522
  this.overflowMenuButton.style.display = "none";
12381
12523
  this.rightButtons.appendChild(this.overflowMenuButton);
@@ -13697,6 +13839,24 @@
13697
13839
  const isVisible = this.player.transcriptManager && this.player.transcriptManager.isVisible;
13698
13840
  this.controls.transcript.setAttribute("aria-expanded", isVisible ? "true" : "false");
13699
13841
  }
13842
+ createHelpButton() {
13843
+ const ariaLabel = i18n.t("help.button");
13844
+ const button = DOMUtils.createElement("button", {
13845
+ className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-help`,
13846
+ attributes: {
13847
+ "type": "button",
13848
+ "aria-label": ariaLabel,
13849
+ "aria-haspopup": "dialog"
13850
+ }
13851
+ });
13852
+ button.appendChild(createIconElement("help"));
13853
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
13854
+ button.addEventListener("click", () => {
13855
+ this.player.toggleKeyboardHelp();
13856
+ });
13857
+ this.controls.help = button;
13858
+ return button;
13859
+ }
13700
13860
  createAudioDescriptionButton() {
13701
13861
  const ariaLabel = i18n.t("player.audioDescription");
13702
13862
  const button = DOMUtils.createElement("button", {
@@ -14944,6 +15104,9 @@
14944
15104
  return true;
14945
15105
  }
14946
15106
  return false;
15107
+ case "help":
15108
+ this.player.toggleKeyboardHelp();
15109
+ return true;
14947
15110
  default:
14948
15111
  return false;
14949
15112
  }
@@ -15031,6 +15194,252 @@
15031
15194
  }
15032
15195
  };
15033
15196
 
15197
+ // src/core/MediaSessionManager.ts
15198
+ var POSITION_THROTTLE_MS = 1e3;
15199
+ var activeManager = null;
15200
+ var MediaSessionManager = class {
15201
+ constructor(player) {
15202
+ __publicField(this, "player");
15203
+ __publicField(this, "supported");
15204
+ __publicField(this, "handlers", {});
15205
+ __publicField(this, "boundActions", []);
15206
+ __publicField(this, "lastPositionUpdate", 0);
15207
+ this.player = player;
15208
+ this.supported = typeof navigator !== "undefined" && "mediaSession" in navigator;
15209
+ if (!this.supported) return;
15210
+ this.attachEvents();
15211
+ if (activeManager === null) {
15212
+ this.claimSession();
15213
+ }
15214
+ }
15215
+ /** Does this manager currently own the global media session? */
15216
+ isActive() {
15217
+ return activeManager === this;
15218
+ }
15219
+ /**
15220
+ * Take ownership of the global session: (re)register the action handlers so
15221
+ * the OS controls drive this player, and refresh metadata/state/position.
15222
+ */
15223
+ claimSession() {
15224
+ if (!this.supported) return;
15225
+ activeManager = this;
15226
+ this.setupActionHandlers();
15227
+ this.updateMetadata();
15228
+ this.updatePlaybackState();
15229
+ this.updatePositionState(true);
15230
+ }
15231
+ get session() {
15232
+ return navigator.mediaSession;
15233
+ }
15234
+ setActionHandler(action, handler) {
15235
+ try {
15236
+ this.session.setActionHandler(action, handler);
15237
+ if (handler && !this.boundActions.includes(action)) {
15238
+ this.boundActions.push(action);
15239
+ }
15240
+ } catch {
15241
+ }
15242
+ }
15243
+ setupActionHandlers() {
15244
+ this.setActionHandler("play", () => this.player.play());
15245
+ this.setActionHandler("pause", () => this.player.pause());
15246
+ this.setActionHandler("stop", () => this.player.stop());
15247
+ this.setActionHandler("seekbackward", (details) => {
15248
+ this.player.seekBackward(this.offsetFrom(details));
15249
+ });
15250
+ this.setActionHandler("seekforward", (details) => {
15251
+ this.player.seekForward(this.offsetFrom(details));
15252
+ });
15253
+ this.setActionHandler("seekto", (details) => {
15254
+ if (details && typeof details.seekTime === "number") {
15255
+ this.player.seek(details.seekTime);
15256
+ }
15257
+ });
15258
+ this.updateTrackHandlers();
15259
+ }
15260
+ offsetFrom(details) {
15261
+ const offset = details && typeof details.seekOffset === "number" ? details.seekOffset : void 0;
15262
+ return typeof offset === "number" && offset > 0 ? offset : void 0;
15263
+ }
15264
+ /**
15265
+ * previous/next track only make sense with a multi-item playlist; bind
15266
+ * or clear them whenever the playlist state changes so the OS shows the
15267
+ * correct affordances.
15268
+ */
15269
+ updateTrackHandlers() {
15270
+ const pm = this.player.playlistManager;
15271
+ const hasPlaylist = Boolean(pm && Array.isArray(pm.tracks) && pm.tracks.length > 1);
15272
+ if (hasPlaylist && pm) {
15273
+ this.setActionHandler("previoustrack", () => pm.previous());
15274
+ this.setActionHandler("nexttrack", () => pm.next());
15275
+ } else {
15276
+ this.setActionHandler("previoustrack", null);
15277
+ this.setActionHandler("nexttrack", null);
15278
+ }
15279
+ }
15280
+ attachEvents() {
15281
+ this.handlers = {
15282
+ // Starting playback makes this player the session owner, taking over
15283
+ // the OS controls from any other player on the page.
15284
+ play: () => this.claimSession(),
15285
+ pause: () => {
15286
+ if (!this.isActive()) return;
15287
+ this.updatePlaybackState();
15288
+ this.updatePositionState(true);
15289
+ },
15290
+ ended: () => {
15291
+ if (!this.isActive()) return;
15292
+ this.updatePlaybackState();
15293
+ },
15294
+ timeupdate: () => {
15295
+ if (!this.isActive()) return;
15296
+ this.updatePositionState();
15297
+ },
15298
+ durationchange: () => {
15299
+ if (!this.isActive()) return;
15300
+ this.updatePositionState(true);
15301
+ },
15302
+ ratechange: () => {
15303
+ if (!this.isActive()) return;
15304
+ this.updatePositionState(true);
15305
+ },
15306
+ loadedmetadata: () => {
15307
+ if (!this.isActive()) return;
15308
+ this.updateMetadata();
15309
+ this.updatePositionState(true);
15310
+ },
15311
+ playlisttrackchange: () => {
15312
+ if (!this.isActive()) return;
15313
+ this.updateMetadata();
15314
+ this.updateTrackHandlers();
15315
+ this.updatePositionState(true);
15316
+ }
15317
+ };
15318
+ for (const [event, handler] of Object.entries(this.handlers)) {
15319
+ if (handler) {
15320
+ this.player.on(event, handler);
15321
+ }
15322
+ }
15323
+ }
15324
+ resolveMetadata() {
15325
+ const opts = this.player.options;
15326
+ let title = opts.title || "";
15327
+ let artist = opts.artist || "";
15328
+ const album = opts.album || "";
15329
+ let poster = opts.poster || null;
15330
+ const pm = this.player.playlistManager;
15331
+ if (pm && Array.isArray(pm.tracks) && pm.currentIndex >= 0) {
15332
+ const track = pm.tracks[pm.currentIndex];
15333
+ if (track) {
15334
+ if (track.title) title = track.title;
15335
+ if (track.artist) artist = track.artist;
15336
+ if (track.poster) poster = track.poster;
15337
+ }
15338
+ }
15339
+ if (!title && typeof document !== "undefined") {
15340
+ title = document.title || "VidPly";
15341
+ }
15342
+ return { title, artist, album, poster };
15343
+ }
15344
+ updateMetadata() {
15345
+ if (!this.supported || typeof window === "undefined" || typeof window.MediaMetadata === "undefined") {
15346
+ return;
15347
+ }
15348
+ const { title, artist, album, poster } = this.resolveMetadata();
15349
+ const artwork = [];
15350
+ if (poster) {
15351
+ try {
15352
+ const src = this.player.resolvePosterPath(poster);
15353
+ if (src) {
15354
+ artwork.push({ src });
15355
+ }
15356
+ } catch {
15357
+ }
15358
+ }
15359
+ try {
15360
+ this.session.metadata = new window.MediaMetadata({
15361
+ title,
15362
+ artist,
15363
+ album,
15364
+ artwork
15365
+ });
15366
+ } catch {
15367
+ }
15368
+ }
15369
+ updatePlaybackState() {
15370
+ if (!this.supported) return;
15371
+ try {
15372
+ this.session.playbackState = this.player.state.playing ? "playing" : "paused";
15373
+ } catch {
15374
+ }
15375
+ }
15376
+ /**
15377
+ * Push the current position to the OS scrubber. `timeupdate` fires
15378
+ * several times a second, so non-forced updates are throttled.
15379
+ */
15380
+ updatePositionState(force = false) {
15381
+ if (!this.supported || typeof this.session.setPositionState !== "function") {
15382
+ return;
15383
+ }
15384
+ const now = Date.now();
15385
+ if (!force && now - this.lastPositionUpdate < POSITION_THROTTLE_MS) {
15386
+ return;
15387
+ }
15388
+ this.lastPositionUpdate = now;
15389
+ const duration = this.player.state.duration;
15390
+ if (!Number.isFinite(duration) || duration <= 0) {
15391
+ try {
15392
+ this.session.setPositionState();
15393
+ } catch {
15394
+ }
15395
+ return;
15396
+ }
15397
+ const playbackRate = this.player.state.playbackSpeed || 1;
15398
+ const position = Math.min(Math.max(0, this.player.state.currentTime || 0), duration);
15399
+ try {
15400
+ this.session.setPositionState({
15401
+ duration,
15402
+ playbackRate: playbackRate > 0 ? playbackRate : 1,
15403
+ position
15404
+ });
15405
+ } catch {
15406
+ }
15407
+ }
15408
+ destroy() {
15409
+ if (!this.supported) return;
15410
+ for (const [event, handler] of Object.entries(this.handlers)) {
15411
+ if (handler) {
15412
+ this.player.off(event, handler);
15413
+ }
15414
+ }
15415
+ this.handlers = {};
15416
+ if (this.isActive()) {
15417
+ for (const action of this.boundActions) {
15418
+ try {
15419
+ this.session.setActionHandler(action, null);
15420
+ } catch {
15421
+ }
15422
+ }
15423
+ try {
15424
+ this.session.metadata = null;
15425
+ } catch {
15426
+ }
15427
+ try {
15428
+ if (typeof this.session.setPositionState === "function") {
15429
+ this.session.setPositionState();
15430
+ }
15431
+ } catch {
15432
+ }
15433
+ try {
15434
+ this.session.playbackState = "none";
15435
+ } catch {
15436
+ }
15437
+ activeManager = null;
15438
+ }
15439
+ this.boundActions = [];
15440
+ }
15441
+ };
15442
+
15034
15443
  // src/core/Player.ts
15035
15444
  init_HTML5Renderer();
15036
15445
  init_Icons();
@@ -16147,6 +16556,263 @@
16147
16556
  }
16148
16557
  };
16149
16558
 
16559
+ // src/controls/KeyboardHelp.ts
16560
+ init_DOMUtils();
16561
+ init_Icons();
16562
+ init_i18n();
16563
+ var ACTION_ORDER = [
16564
+ "play-pause",
16565
+ "seek-backward",
16566
+ "seek-forward",
16567
+ "volume-up",
16568
+ "volume-down",
16569
+ "mute",
16570
+ "captions",
16571
+ "caption-style-menu",
16572
+ "speed-down",
16573
+ "speed-up",
16574
+ "speed-menu",
16575
+ "quality-menu",
16576
+ "chapters-menu",
16577
+ "transcript-toggle",
16578
+ "fullscreen",
16579
+ "help"
16580
+ ];
16581
+ var ACTION_REQUIRES_CONTROL = {
16582
+ captions: "captions",
16583
+ "caption-style-menu": "captionStyle",
16584
+ "speed-down": "speed",
16585
+ "speed-up": "speed",
16586
+ "speed-menu": "speed",
16587
+ "quality-menu": "quality",
16588
+ "chapters-menu": "chapters",
16589
+ "transcript-toggle": "transcript",
16590
+ fullscreen: "fullscreen"
16591
+ };
16592
+ var KeyboardHelp = class {
16593
+ constructor(player) {
16594
+ __publicField(this, "player");
16595
+ __publicField(this, "isOpen", false);
16596
+ __publicField(this, "overlay", null);
16597
+ __publicField(this, "_triggerElement", null);
16598
+ __publicField(this, "_keydownHandler", null);
16599
+ __publicField(this, "_content", null);
16600
+ this.player = player;
16601
+ }
16602
+ get prefix() {
16603
+ return this.player.options.classPrefix;
16604
+ }
16605
+ /**
16606
+ * Turn a raw KeyboardEvent.key value into a human-readable label. Arrow
16607
+ * keys become universally understood glyphs; the space bar and single
16608
+ * letters are normalised for legibility.
16609
+ */
16610
+ formatKey(key) {
16611
+ switch (key) {
16612
+ case " ":
16613
+ return i18n.t("help.keys.space");
16614
+ case "ArrowUp":
16615
+ return "↑";
16616
+ case "ArrowDown":
16617
+ return "↓";
16618
+ case "ArrowLeft":
16619
+ return "←";
16620
+ case "ArrowRight":
16621
+ return "→";
16622
+ case "Escape":
16623
+ return "Esc";
16624
+ default:
16625
+ return key.length === 1 ? key.toUpperCase() : key;
16626
+ }
16627
+ }
16628
+ createElement() {
16629
+ const titleId = `${this.prefix}-help-title-${this.player.instanceId}`;
16630
+ const overlay = DOMUtils.createElement("div", {
16631
+ className: `${this.prefix}-settings-overlay ${this.prefix}-help-overlay`,
16632
+ attributes: {
16633
+ role: "dialog",
16634
+ "aria-modal": "true",
16635
+ "aria-labelledby": titleId
16636
+ }
16637
+ });
16638
+ overlay.style.display = "none";
16639
+ const dialog = DOMUtils.createElement("div", {
16640
+ className: `${this.prefix}-settings-dialog ${this.prefix}-help-dialog`
16641
+ });
16642
+ const header = DOMUtils.createElement("div", {
16643
+ className: `${this.prefix}-settings-header`
16644
+ });
16645
+ const title = DOMUtils.createElement("h2", {
16646
+ textContent: i18n.t("help.title"),
16647
+ attributes: { id: titleId }
16648
+ });
16649
+ const closeButton = DOMUtils.createElement("button", {
16650
+ className: `${this.prefix}-button ${this.prefix}-settings-close`,
16651
+ attributes: {
16652
+ type: "button",
16653
+ "aria-label": i18n.t("help.close")
16654
+ }
16655
+ });
16656
+ closeButton.appendChild(createIconElement("close"));
16657
+ closeButton.addEventListener("click", () => this.hide());
16658
+ header.appendChild(title);
16659
+ header.appendChild(closeButton);
16660
+ const content = DOMUtils.createElement("div", {
16661
+ className: `${this.prefix}-settings-content`
16662
+ });
16663
+ this._content = content;
16664
+ content.appendChild(this.buildShortcutList());
16665
+ dialog.appendChild(header);
16666
+ dialog.appendChild(content);
16667
+ overlay.appendChild(dialog);
16668
+ overlay.addEventListener("click", (e) => {
16669
+ if (e.target === overlay) {
16670
+ this.hide();
16671
+ }
16672
+ });
16673
+ this._keydownHandler = (e) => {
16674
+ if (!this.isOpen || !this.overlay) return;
16675
+ if (e.key === "Escape") {
16676
+ e.preventDefault();
16677
+ this.hide();
16678
+ return;
16679
+ }
16680
+ if (e.key === "Tab") {
16681
+ const focusable = Array.from(
16682
+ this.overlay.querySelectorAll(
16683
+ 'button, [href], select, input, textarea, [tabindex]:not([tabindex="-1"])'
16684
+ )
16685
+ ).filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
16686
+ if (focusable.length === 0) return;
16687
+ const first = focusable[0];
16688
+ const last = focusable[focusable.length - 1];
16689
+ if (!first || !last) return;
16690
+ const active = document.activeElement;
16691
+ if (e.shiftKey && (active === first || !this.overlay.contains(active))) {
16692
+ e.preventDefault();
16693
+ last.focus({ preventScroll: true });
16694
+ } else if (!e.shiftKey && active === last) {
16695
+ e.preventDefault();
16696
+ first.focus({ preventScroll: true });
16697
+ }
16698
+ }
16699
+ };
16700
+ const lifecycleSignal = this.player.lifecycleSignal;
16701
+ document.addEventListener(
16702
+ "keydown",
16703
+ this._keydownHandler,
16704
+ lifecycleSignal ? { signal: lifecycleSignal } : void 0
16705
+ );
16706
+ return overlay;
16707
+ }
16708
+ /**
16709
+ * Whether a shortcut row is worth showing for *this* player. Feature actions
16710
+ * are hidden when their control isn't present (e.g. no captions track, an
16711
+ * audio-only player with no fullscreen). Core actions are always relevant.
16712
+ *
16713
+ * When the player has no control bar we can't infer availability, so nothing
16714
+ * is hidden — the shortcuts still work and we'd rather over-show than mislead.
16715
+ */
16716
+ isActionRelevant(action) {
16717
+ const requiredControl = ACTION_REQUIRES_CONTROL[action];
16718
+ if (!requiredControl) return true;
16719
+ const controlBar = this.player.controlBar;
16720
+ if (!controlBar || !controlBar.controls) return true;
16721
+ return Boolean(controlBar.controls[requiredControl]);
16722
+ }
16723
+ buildShortcutList() {
16724
+ const list = DOMUtils.createElement("dl", {
16725
+ className: `${this.prefix}-help-list`
16726
+ });
16727
+ const shortcuts = this.player.options.keyboardShortcuts;
16728
+ for (const action of ACTION_ORDER) {
16729
+ const keys = shortcuts[action];
16730
+ if (!Array.isArray(keys) || keys.length === 0) continue;
16731
+ if (!this.isActionRelevant(action)) continue;
16732
+ const term = DOMUtils.createElement("dt", {
16733
+ className: `${this.prefix}-help-action`,
16734
+ textContent: i18n.t(`help.actions.${action}`)
16735
+ });
16736
+ const desc = DOMUtils.createElement("dd", {
16737
+ className: `${this.prefix}-help-keys`
16738
+ });
16739
+ keys.forEach((key, index) => {
16740
+ if (index > 0) {
16741
+ desc.appendChild(
16742
+ DOMUtils.createElement("span", {
16743
+ className: `${this.prefix}-help-key-sep`,
16744
+ textContent: i18n.t("help.or")
16745
+ })
16746
+ );
16747
+ }
16748
+ desc.appendChild(
16749
+ DOMUtils.createElement("kbd", {
16750
+ className: `${this.prefix}-help-key`,
16751
+ textContent: this.formatKey(key)
16752
+ })
16753
+ );
16754
+ });
16755
+ list.appendChild(term);
16756
+ list.appendChild(desc);
16757
+ }
16758
+ return list;
16759
+ }
16760
+ show() {
16761
+ if (this.isOpen) return;
16762
+ if (!this.overlay) {
16763
+ this.overlay = this.createElement();
16764
+ this.player.container.appendChild(this.overlay);
16765
+ } else if (this._content) {
16766
+ this._content.replaceChildren(this.buildShortcutList());
16767
+ }
16768
+ const active = typeof document !== "undefined" ? document.activeElement : null;
16769
+ this._triggerElement = active && typeof active.focus === "function" ? active : null;
16770
+ this.overlay.style.display = "flex";
16771
+ this.isOpen = true;
16772
+ const closeButton = this.overlay.querySelector(`.${this.prefix}-settings-close`);
16773
+ closeButton == null ? void 0 : closeButton.focus({ preventScroll: true });
16774
+ this.player.emit("keyboardhelpopen");
16775
+ }
16776
+ hide() {
16777
+ var _a, _b;
16778
+ if (!this.overlay) return;
16779
+ this.overlay.style.display = "none";
16780
+ this.isOpen = false;
16781
+ const trigger = this._triggerElement;
16782
+ this._triggerElement = null;
16783
+ if (trigger && document.contains(trigger)) {
16784
+ try {
16785
+ trigger.focus({ preventScroll: true });
16786
+ } catch {
16787
+ (_a = this.player.container) == null ? void 0 : _a.focus();
16788
+ }
16789
+ } else {
16790
+ (_b = this.player.container) == null ? void 0 : _b.focus();
16791
+ }
16792
+ this.player.emit("keyboardhelpclose");
16793
+ }
16794
+ toggle() {
16795
+ if (this.isOpen) {
16796
+ this.hide();
16797
+ } else {
16798
+ this.show();
16799
+ }
16800
+ }
16801
+ destroy() {
16802
+ if (this._keydownHandler) {
16803
+ document.removeEventListener("keydown", this._keydownHandler);
16804
+ this._keydownHandler = null;
16805
+ }
16806
+ if (this.overlay && this.overlay.parentNode) {
16807
+ this.overlay.parentNode.removeChild(this.overlay);
16808
+ }
16809
+ this.overlay = null;
16810
+ this._content = null;
16811
+ this._triggerElement = null;
16812
+ this.isOpen = false;
16813
+ }
16814
+ };
16815
+
16150
16816
  // src/core/Player.ts
16151
16817
  var AudioDescriptionManagerModule = null;
16152
16818
  var SignLanguageManagerModule = null;
@@ -16190,9 +16856,11 @@
16190
16856
  __publicField(this, "controlBar", null);
16191
16857
  __publicField(this, "captionManager", null);
16192
16858
  __publicField(this, "keyboardManager", null);
16859
+ __publicField(this, "mediaSessionManager", null);
16193
16860
  __publicField(this, "transcriptManager", null);
16194
16861
  __publicField(this, "playlistManager", null);
16195
16862
  __publicField(this, "settingsDialog", null);
16863
+ __publicField(this, "keyboardHelp", null);
16196
16864
  __publicField(this, "audioDescriptionManager", null);
16197
16865
  __publicField(this, "signLanguageManager", null);
16198
16866
  __publicField(this, "floatingPlayerManager", null);
@@ -16306,6 +16974,11 @@
16306
16974
  poster: null,
16307
16975
  responsive: true,
16308
16976
  fillContainer: false,
16977
+ // Media metadata + OS media controls (Media Session API)
16978
+ title: null,
16979
+ artist: null,
16980
+ album: null,
16981
+ mediaSession: true,
16309
16982
  // Playback
16310
16983
  autoplay: false,
16311
16984
  loop: false,
@@ -16353,6 +17026,7 @@
16353
17026
  captionsButton: true,
16354
17027
  transcriptButton: true,
16355
17028
  fullscreenButton: true,
17029
+ helpButton: true,
16356
17030
  pipButton: false,
16357
17031
  floating: false,
16358
17032
  floatingPosition: "bottom-right",
@@ -16411,7 +17085,8 @@
16411
17085
  "speed-menu": ["s"],
16412
17086
  "quality-menu": ["q"],
16413
17087
  "chapters-menu": ["j"],
16414
- "transcript-toggle": ["t"]
17088
+ "transcript-toggle": ["t"],
17089
+ "help": ["?"]
16415
17090
  },
16416
17091
  // Accessibility
16417
17092
  ariaLabels: {},
@@ -16794,6 +17469,9 @@
16794
17469
  if (this.options.keyboard) {
16795
17470
  this.keyboardManager = new KeyboardManager(this);
16796
17471
  }
17472
+ if (this.options.mediaSession) {
17473
+ this.mediaSessionManager = new MediaSessionManager(this);
17474
+ }
16797
17475
  this.setupResponsiveHandlers();
16798
17476
  if (this.options.startTime > 0) {
16799
17477
  this.seek(this.options.startTime);
@@ -18135,6 +18813,27 @@
18135
18813
  }
18136
18814
  hideSettings() {
18137
18815
  }
18816
+ /**
18817
+ * Lazily build (on first use) and toggle the keyboard-shortcuts help
18818
+ * dialog. Reflects the live `keyboardShortcuts` bindings, including any
18819
+ * consumer overrides.
18820
+ */
18821
+ toggleKeyboardHelp() {
18822
+ if (!this.keyboardHelp) {
18823
+ this.keyboardHelp = new KeyboardHelp(this);
18824
+ }
18825
+ this.keyboardHelp.toggle();
18826
+ }
18827
+ showKeyboardHelp() {
18828
+ if (!this.keyboardHelp) {
18829
+ this.keyboardHelp = new KeyboardHelp(this);
18830
+ }
18831
+ this.keyboardHelp.show();
18832
+ }
18833
+ hideKeyboardHelp() {
18834
+ var _a;
18835
+ (_a = this.keyboardHelp) == null ? void 0 : _a.hide();
18836
+ }
18138
18837
  // Utility methods
18139
18838
  getCurrentTime() {
18140
18839
  return this.state.currentTime;
@@ -18275,6 +18974,22 @@
18275
18974
  }
18276
18975
  this.settingsDialog = null;
18277
18976
  }
18977
+ if (this.keyboardHelp && typeof this.keyboardHelp.destroy === "function") {
18978
+ try {
18979
+ this.keyboardHelp.destroy();
18980
+ } catch (err) {
18981
+ this.log(`KeyboardHelp.destroy failed: ${err}`, "warn");
18982
+ }
18983
+ this.keyboardHelp = null;
18984
+ }
18985
+ if (this.mediaSessionManager && typeof this.mediaSessionManager.destroy === "function") {
18986
+ try {
18987
+ this.mediaSessionManager.destroy();
18988
+ } catch (err) {
18989
+ this.log(`MediaSessionManager.destroy failed: ${err}`, "warn");
18990
+ }
18991
+ this.mediaSessionManager = null;
18992
+ }
18278
18993
  if (this.floatingPlayerManager) {
18279
18994
  try {
18280
18995
  this.floatingPlayerManager.destroy();