vidply 1.2.0 → 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 (167) hide show
  1. package/README.md +974 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
  23. package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
  24. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
  25. package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
  26. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
  27. package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
  28. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
  30. package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
  32. package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
  34. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
  35. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
  36. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
  37. package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
  38. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
  39. package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
  41. package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
  43. package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +648 -35
  45. package/dist/dev/vidply.esm.js.map +3 -3
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
  47. package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
  49. package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
  50. package/dist/legacy/vidply.js +1062 -34
  51. package/dist/legacy/vidply.js.map +3 -3
  52. package/dist/legacy/vidply.min.js +2 -2
  53. package/dist/legacy/vidply.min.meta.json +127 -55
  54. package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
  66. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
  67. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
  69. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
  72. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
  75. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
  81. package/dist/vidply.css +5852 -5796
  82. package/dist/vidply.esm.min.meta.json +204 -149
  83. package/dist/vidply.min.css +1 -1
  84. package/package.json +11 -11
  85. package/src/controls/CaptionManager.ts +598 -598
  86. package/src/controls/ControlBar.ts +4191 -4159
  87. package/src/controls/KeyboardHelp.ts +326 -0
  88. package/src/controls/KeyboardManager.ts +387 -383
  89. package/src/controls/SettingsDialog.ts +488 -488
  90. package/src/controls/TranscriptManager.ts +2426 -2426
  91. package/src/core/AudioDescriptionManager.ts +863 -774
  92. package/src/core/DescriptionSpeechManager.ts +232 -0
  93. package/src/core/FloatingPlayerManager.ts +674 -674
  94. package/src/core/LazyInit.ts +80 -80
  95. package/src/core/MediaSessionManager.ts +341 -0
  96. package/src/core/MetadataAlertsManager.ts +514 -514
  97. package/src/core/Player.ts +2973 -2872
  98. package/src/core/PosterManager.ts +169 -169
  99. package/src/core/PseudoFullscreen.ts +179 -179
  100. package/src/core/ResponsiveManager.ts +231 -231
  101. package/src/core/ResumeManager.ts +236 -236
  102. package/src/core/SignLanguageManager.ts +1308 -1308
  103. package/src/core/ThemeManager.ts +157 -157
  104. package/src/features/PlaylistManager.ts +1838 -1827
  105. package/src/i18n/i18n.ts +223 -223
  106. package/src/i18n/languages/de.ts +244 -217
  107. package/src/i18n/languages/en.ts +245 -218
  108. package/src/i18n/languages/es.ts +241 -214
  109. package/src/i18n/languages/fr.ts +241 -214
  110. package/src/i18n/languages/ja.ts +241 -214
  111. package/src/icons/Icons.ts +168 -167
  112. package/src/index.ts +196 -193
  113. package/src/renderers/DASHRenderer.ts +1077 -1077
  114. package/src/renderers/HLSRenderer.ts +944 -944
  115. package/src/renderers/HTML5Renderer.ts +389 -389
  116. package/src/renderers/SoundCloudRenderer.ts +437 -437
  117. package/src/renderers/VimeoRenderer.ts +298 -298
  118. package/src/renderers/YouTubeRenderer.ts +290 -290
  119. package/src/styles/vidply.css +5852 -5796
  120. package/src/types/events.ts +128 -122
  121. package/src/types/globals.d.ts +313 -313
  122. package/src/types/options.ts +199 -182
  123. package/src/types/renderer.ts +58 -58
  124. package/src/utils/DOMUtils.ts +281 -281
  125. package/src/utils/DownloadInfo.ts +171 -171
  126. package/src/utils/DraggablePanel.ts +539 -539
  127. package/src/utils/DraggablePanelMenu.ts +195 -195
  128. package/src/utils/DraggableResizable.ts +997 -997
  129. package/src/utils/EventEmitter.ts +59 -59
  130. package/src/utils/FormUtils.ts +95 -95
  131. package/src/utils/MenuFactory.ts +483 -483
  132. package/src/utils/Sanitize.ts +70 -70
  133. package/src/utils/SettingsMenuFactory.ts +289 -289
  134. package/src/utils/StorageManager.ts +287 -287
  135. package/src/utils/TrackLabelUtils.ts +52 -52
  136. package/src/utils/UrlSafe.ts +69 -69
  137. package/src/utils/VideoFrameCapture.ts +114 -114
  138. package/src/utils/WindowComponents.ts +250 -250
  139. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  140. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  141. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  142. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  143. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  144. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  145. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  146. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  147. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  148. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  149. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  151. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  152. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  153. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  154. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  155. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  156. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  157. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  158. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  159. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  160. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  161. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  162. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
  163. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
  165. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
  166. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
  167. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
@@ -1,80 +1,80 @@
1
- /**
2
- * Lazy instantiation helpers for {@link Player}.
3
- *
4
- * Extracted from `index.ts` so that:
5
- *
6
- * 1. The `Player.observeLazy` public API is a real static method on the
7
- * Player class rather than a side-effectful assignment done from
8
- * `index.ts` at module load time.
9
- * 2. Consumers that want the lazy-init API without the auto-init
10
- * entry point can import this module explicitly.
11
- * 3. Both the auto-init flow (`initializePlayers`) and the manual flow
12
- * (`Player.observeLazy`) share the same `IntersectionObserver`
13
- * bookkeeping, so cancellation and rect-based eager fallbacks stay
14
- * consistent.
15
- */
16
-
17
- export type LazyHandle = { cancel: () => void } | null;
18
-
19
- export interface LazyInitEntry<TOptions> {
20
- observer: IntersectionObserver;
21
- options: TOptions;
22
- }
23
-
24
- /** Factory the observer invokes once an element is ready to promote. */
25
- export type LazyPlayerFactory<TOptions> = (
26
- element: HTMLElement,
27
- options: TOptions
28
- ) => void;
29
-
30
- const pendingByElement = new WeakMap<HTMLElement, LazyInitEntry<unknown>>();
31
-
32
- /**
33
- * Begin observing `element` and construct a player when it scrolls into
34
- * view (or immediately if its rect is too small to be a meaningful
35
- * intersection target). Safe to call multiple times — a duplicate call
36
- * cancels the previous observer first.
37
- */
38
- export function observeForLazyInit<TOptions>(
39
- element: HTMLElement,
40
- options: TOptions,
41
- margin: string,
42
- factory: LazyPlayerFactory<TOptions>
43
- ): void {
44
- const existing = pendingByElement.get(element);
45
- if (existing) {
46
- existing.observer.unobserve(element);
47
- pendingByElement.delete(element);
48
- }
49
-
50
- const rect = element.getBoundingClientRect();
51
- if (rect.height < 20) {
52
- factory(element, options);
53
- return;
54
- }
55
-
56
- const observer = new IntersectionObserver(
57
- (entries) => {
58
- entries.forEach((entry) => {
59
- if (entry.isIntersecting) {
60
- observer.unobserve(entry.target);
61
- pendingByElement.delete(entry.target as HTMLElement);
62
- factory(entry.target as HTMLElement, options);
63
- }
64
- });
65
- },
66
- { rootMargin: margin, threshold: 0 }
67
- );
68
-
69
- observer.observe(element);
70
- pendingByElement.set(element, { observer, options } as LazyInitEntry<unknown>);
71
- }
72
-
73
- /** Cancel a scheduled lazy init for `element`, if any. */
74
- export function cancelLazyInit(element: HTMLElement): void {
75
- const pending = pendingByElement.get(element);
76
- if (pending) {
77
- pending.observer.unobserve(element);
78
- pendingByElement.delete(element);
79
- }
80
- }
1
+ /**
2
+ * Lazy instantiation helpers for {@link Player}.
3
+ *
4
+ * Extracted from `index.ts` so that:
5
+ *
6
+ * 1. The `Player.observeLazy` public API is a real static method on the
7
+ * Player class rather than a side-effectful assignment done from
8
+ * `index.ts` at module load time.
9
+ * 2. Consumers that want the lazy-init API without the auto-init
10
+ * entry point can import this module explicitly.
11
+ * 3. Both the auto-init flow (`initializePlayers`) and the manual flow
12
+ * (`Player.observeLazy`) share the same `IntersectionObserver`
13
+ * bookkeeping, so cancellation and rect-based eager fallbacks stay
14
+ * consistent.
15
+ */
16
+
17
+ export type LazyHandle = { cancel: () => void } | null;
18
+
19
+ export interface LazyInitEntry<TOptions> {
20
+ observer: IntersectionObserver;
21
+ options: TOptions;
22
+ }
23
+
24
+ /** Factory the observer invokes once an element is ready to promote. */
25
+ export type LazyPlayerFactory<TOptions> = (
26
+ element: HTMLElement,
27
+ options: TOptions
28
+ ) => void;
29
+
30
+ const pendingByElement = new WeakMap<HTMLElement, LazyInitEntry<unknown>>();
31
+
32
+ /**
33
+ * Begin observing `element` and construct a player when it scrolls into
34
+ * view (or immediately if its rect is too small to be a meaningful
35
+ * intersection target). Safe to call multiple times — a duplicate call
36
+ * cancels the previous observer first.
37
+ */
38
+ export function observeForLazyInit<TOptions>(
39
+ element: HTMLElement,
40
+ options: TOptions,
41
+ margin: string,
42
+ factory: LazyPlayerFactory<TOptions>
43
+ ): void {
44
+ const existing = pendingByElement.get(element);
45
+ if (existing) {
46
+ existing.observer.unobserve(element);
47
+ pendingByElement.delete(element);
48
+ }
49
+
50
+ const rect = element.getBoundingClientRect();
51
+ if (rect.height < 20) {
52
+ factory(element, options);
53
+ return;
54
+ }
55
+
56
+ const observer = new IntersectionObserver(
57
+ (entries) => {
58
+ entries.forEach((entry) => {
59
+ if (entry.isIntersecting) {
60
+ observer.unobserve(entry.target);
61
+ pendingByElement.delete(entry.target as HTMLElement);
62
+ factory(entry.target as HTMLElement, options);
63
+ }
64
+ });
65
+ },
66
+ { rootMargin: margin, threshold: 0 }
67
+ );
68
+
69
+ observer.observe(element);
70
+ pendingByElement.set(element, { observer, options } as LazyInitEntry<unknown>);
71
+ }
72
+
73
+ /** Cancel a scheduled lazy init for `element`, if any. */
74
+ export function cancelLazyInit(element: HTMLElement): void {
75
+ const pending = pendingByElement.get(element);
76
+ if (pending) {
77
+ pending.observer.unobserve(element);
78
+ pendingByElement.delete(element);
79
+ }
80
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Media Session API integration.
3
+ *
4
+ * Wires the player into `navigator.mediaSession` so the OS shows
5
+ * now-playing metadata (title / artist / album / artwork) and routes
6
+ * hardware/lock-screen/notification controls (play, pause, stop, seek,
7
+ * and previous/next track for playlists) back into the player.
8
+ *
9
+ * Everything is feature-detected and wrapped in try/catch: unsupported
10
+ * browsers and unsupported individual actions degrade silently.
11
+ */
12
+
13
+ import type { Player } from './Player.js';
14
+
15
+ interface PlaylistLikeTrack {
16
+ title?: string;
17
+ artist?: string;
18
+ poster?: string;
19
+ }
20
+
21
+ const POSITION_THROTTLE_MS = 1000;
22
+
23
+ /**
24
+ * `navigator.mediaSession` is a single, global object shared by the whole
25
+ * document, but a page can host several VidPly players at once (common in a
26
+ * CMS). Only one of them may own the session at a time, otherwise the OS
27
+ * controls would be wired to a player the user isn't actually listening to
28
+ * (clicking play/pause/next then appears to "do nothing").
29
+ *
30
+ * We track the owning manager here and follow the browser convention: the
31
+ * player that most recently started playing claims the session. Background
32
+ * players never touch the global session, and a player only releases it on
33
+ * destroy if it still owns it.
34
+ */
35
+ let activeManager: MediaSessionManager | null = null;
36
+
37
+ export class MediaSessionManager {
38
+ player: Player;
39
+ private supported: boolean;
40
+ private handlers: Partial<Record<string, () => void>> = {};
41
+ private boundActions: string[] = [];
42
+ private lastPositionUpdate = 0;
43
+
44
+ constructor(player: Player) {
45
+ this.player = player;
46
+ this.supported =
47
+ typeof navigator !== 'undefined' && 'mediaSession' in navigator;
48
+
49
+ if (!this.supported) return;
50
+
51
+ this.attachEvents();
52
+
53
+ // Claim the session up front only when no other player owns it yet (i.e.
54
+ // we're the first/only player on the page). When other players already
55
+ // exist we wait until this one actually starts playing before taking
56
+ // over, so we never hijack a player the user is currently using.
57
+ if (activeManager === null) {
58
+ this.claimSession();
59
+ }
60
+ }
61
+
62
+ /** Does this manager currently own the global media session? */
63
+ private isActive(): boolean {
64
+ return activeManager === this;
65
+ }
66
+
67
+ /**
68
+ * Take ownership of the global session: (re)register the action handlers so
69
+ * the OS controls drive this player, and refresh metadata/state/position.
70
+ */
71
+ private claimSession(): void {
72
+ if (!this.supported) return;
73
+ activeManager = this;
74
+ this.setupActionHandlers();
75
+ this.updateMetadata();
76
+ this.updatePlaybackState();
77
+ this.updatePositionState(true);
78
+ }
79
+
80
+ private get session(): MediaSession {
81
+ return navigator.mediaSession;
82
+ }
83
+
84
+ private setActionHandler(
85
+ action: MediaSessionAction,
86
+ handler: MediaSessionActionHandler | null
87
+ ): void {
88
+ try {
89
+ this.session.setActionHandler(action, handler);
90
+ if (handler && !this.boundActions.includes(action)) {
91
+ this.boundActions.push(action);
92
+ }
93
+ } catch {
94
+ // Action not supported by this browser - ignore.
95
+ }
96
+ }
97
+
98
+ private setupActionHandlers(): void {
99
+ this.setActionHandler('play', () => this.player.play());
100
+ this.setActionHandler('pause', () => this.player.pause());
101
+ this.setActionHandler('stop', () => this.player.stop());
102
+
103
+ this.setActionHandler('seekbackward', (details) => {
104
+ this.player.seekBackward(this.offsetFrom(details));
105
+ });
106
+ this.setActionHandler('seekforward', (details) => {
107
+ this.player.seekForward(this.offsetFrom(details));
108
+ });
109
+ this.setActionHandler('seekto', (details) => {
110
+ if (details && typeof details.seekTime === 'number') {
111
+ this.player.seek(details.seekTime);
112
+ }
113
+ });
114
+
115
+ this.updateTrackHandlers();
116
+ }
117
+
118
+ private offsetFrom(details?: MediaSessionActionDetails): number | undefined {
119
+ const offset = details && typeof details.seekOffset === 'number' ? details.seekOffset : undefined;
120
+ return typeof offset === 'number' && offset > 0 ? offset : undefined;
121
+ }
122
+
123
+ /**
124
+ * previous/next track only make sense with a multi-item playlist; bind
125
+ * or clear them whenever the playlist state changes so the OS shows the
126
+ * correct affordances.
127
+ */
128
+ private updateTrackHandlers(): void {
129
+ const pm = this.player.playlistManager;
130
+ const hasPlaylist = Boolean(pm && Array.isArray(pm.tracks) && pm.tracks.length > 1);
131
+
132
+ if (hasPlaylist && pm) {
133
+ this.setActionHandler('previoustrack', () => pm.previous());
134
+ this.setActionHandler('nexttrack', () => pm.next());
135
+ } else {
136
+ this.setActionHandler('previoustrack', null);
137
+ this.setActionHandler('nexttrack', null);
138
+ }
139
+ }
140
+
141
+ private attachEvents(): void {
142
+ this.handlers = {
143
+ // Starting playback makes this player the session owner, taking over
144
+ // the OS controls from any other player on the page.
145
+ play: () => this.claimSession(),
146
+ pause: () => {
147
+ if (!this.isActive()) return;
148
+ this.updatePlaybackState();
149
+ this.updatePositionState(true);
150
+ },
151
+ ended: () => {
152
+ if (!this.isActive()) return;
153
+ this.updatePlaybackState();
154
+ },
155
+ timeupdate: () => {
156
+ if (!this.isActive()) return;
157
+ this.updatePositionState();
158
+ },
159
+ durationchange: () => {
160
+ if (!this.isActive()) return;
161
+ this.updatePositionState(true);
162
+ },
163
+ ratechange: () => {
164
+ if (!this.isActive()) return;
165
+ this.updatePositionState(true);
166
+ },
167
+ loadedmetadata: () => {
168
+ if (!this.isActive()) return;
169
+ this.updateMetadata();
170
+ this.updatePositionState(true);
171
+ },
172
+ playlisttrackchange: () => {
173
+ if (!this.isActive()) return;
174
+ this.updateMetadata();
175
+ this.updateTrackHandlers();
176
+ this.updatePositionState(true);
177
+ }
178
+ };
179
+
180
+ for (const [event, handler] of Object.entries(this.handlers)) {
181
+ if (handler) {
182
+ this.player.on(event as never, handler as never);
183
+ }
184
+ }
185
+ }
186
+
187
+ private resolveMetadata(): {
188
+ title: string;
189
+ artist: string;
190
+ album: string;
191
+ poster: string | null;
192
+ } {
193
+ const opts = this.player.options;
194
+ let title = opts.title || '';
195
+ let artist = opts.artist || '';
196
+ const album = opts.album || '';
197
+ let poster = opts.poster || null;
198
+
199
+ const pm = this.player.playlistManager;
200
+ if (pm && Array.isArray(pm.tracks) && pm.currentIndex >= 0) {
201
+ const track = pm.tracks[pm.currentIndex] as PlaylistLikeTrack | undefined;
202
+ if (track) {
203
+ if (track.title) title = track.title;
204
+ if (track.artist) artist = track.artist;
205
+ if (track.poster) poster = track.poster;
206
+ }
207
+ }
208
+
209
+ if (!title && typeof document !== 'undefined') {
210
+ title = document.title || 'VidPly';
211
+ }
212
+
213
+ return { title, artist, album, poster };
214
+ }
215
+
216
+ updateMetadata(): void {
217
+ if (!this.supported || typeof window === 'undefined' || typeof window.MediaMetadata === 'undefined') {
218
+ return;
219
+ }
220
+
221
+ const { title, artist, album, poster } = this.resolveMetadata();
222
+ const artwork: MediaImage[] = [];
223
+
224
+ if (poster) {
225
+ try {
226
+ const src = this.player.resolvePosterPath(poster);
227
+ if (src) {
228
+ artwork.push({ src });
229
+ }
230
+ } catch {
231
+ // Ignore poster resolution failures.
232
+ }
233
+ }
234
+
235
+ try {
236
+ this.session.metadata = new window.MediaMetadata({
237
+ title,
238
+ artist,
239
+ album,
240
+ artwork
241
+ });
242
+ } catch {
243
+ // Ignore metadata construction failures.
244
+ }
245
+ }
246
+
247
+ updatePlaybackState(): void {
248
+ if (!this.supported) return;
249
+ try {
250
+ this.session.playbackState = this.player.state.playing ? 'playing' : 'paused';
251
+ } catch {
252
+ // Ignore.
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Push the current position to the OS scrubber. `timeupdate` fires
258
+ * several times a second, so non-forced updates are throttled.
259
+ */
260
+ updatePositionState(force = false): void {
261
+ if (!this.supported || typeof this.session.setPositionState !== 'function') {
262
+ return;
263
+ }
264
+
265
+ const now = Date.now();
266
+ if (!force && now - this.lastPositionUpdate < POSITION_THROTTLE_MS) {
267
+ return;
268
+ }
269
+ this.lastPositionUpdate = now;
270
+
271
+ const duration = this.player.state.duration;
272
+ if (!Number.isFinite(duration) || duration <= 0) {
273
+ // A finite, positive duration is required; clear any stale state
274
+ // (e.g. live streams or before metadata is known).
275
+ try {
276
+ this.session.setPositionState();
277
+ } catch {
278
+ // Ignore.
279
+ }
280
+ return;
281
+ }
282
+
283
+ const playbackRate = this.player.state.playbackSpeed || 1;
284
+ const position = Math.min(Math.max(0, this.player.state.currentTime || 0), duration);
285
+
286
+ try {
287
+ this.session.setPositionState({
288
+ duration,
289
+ playbackRate: playbackRate > 0 ? playbackRate : 1,
290
+ position
291
+ });
292
+ } catch {
293
+ // Ignore (e.g. position > duration race).
294
+ }
295
+ }
296
+
297
+ destroy(): void {
298
+ if (!this.supported) return;
299
+
300
+ for (const [event, handler] of Object.entries(this.handlers)) {
301
+ if (handler) {
302
+ this.player.off(event as never, handler as never);
303
+ }
304
+ }
305
+ this.handlers = {};
306
+
307
+ // Only tear down the global session if we currently own it; otherwise we
308
+ // would wipe the controls of whichever player is actually playing.
309
+ if (this.isActive()) {
310
+ for (const action of this.boundActions) {
311
+ try {
312
+ this.session.setActionHandler(action as MediaSessionAction, null);
313
+ } catch {
314
+ // Ignore.
315
+ }
316
+ }
317
+
318
+ try {
319
+ this.session.metadata = null;
320
+ } catch {
321
+ // Ignore.
322
+ }
323
+ try {
324
+ if (typeof this.session.setPositionState === 'function') {
325
+ this.session.setPositionState();
326
+ }
327
+ } catch {
328
+ // Ignore.
329
+ }
330
+ try {
331
+ this.session.playbackState = 'none';
332
+ } catch {
333
+ // Ignore.
334
+ }
335
+
336
+ activeManager = null;
337
+ }
338
+
339
+ this.boundActions = [];
340
+ }
341
+ }