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,169 +1,169 @@
1
- /**
2
- * Poster / artwork helpers lifted out of Player.
3
- *
4
- * The manager owns:
5
- * - relative -> absolute URL resolution;
6
- * - the canvas-based "grab a frame from the video as poster" logic;
7
- * - the `.vidply-forced-poster` overlay that shows the poster while
8
- * the underlying `<video>` is in a paused / not-yet-loaded state.
9
- *
10
- * Player keeps public delegating methods (`resolvePosterPath`,
11
- * `showPosterOverlay`, etc.) so the existing external API is
12
- * unchanged — only the implementation has moved.
13
- */
14
-
15
- import { captureVideoFrame } from '../utils/VideoFrameCapture.js';
16
- import type { Player } from './Player.js';
17
-
18
- export class PosterManager {
19
- private readonly player: Player;
20
-
21
- constructor(player: Player) {
22
- this.player = player;
23
- }
24
-
25
- /**
26
- * Convert a relative poster path into an absolute URL. Absolute URLs
27
- * (http/https) and root-relative paths (`/foo`) are returned as-is.
28
- * Falls back to the raw string on any parse error — a malformed URL
29
- * is still better than throwing and breaking the caller.
30
- */
31
- resolvePath(posterPath: string | null | undefined): string {
32
- if (!posterPath) return '';
33
-
34
- if (posterPath.match(/^(https?:|\/)/)) {
35
- return posterPath;
36
- }
37
-
38
- try {
39
- const posterUrl = new URL(posterPath, window.location.href);
40
- return posterUrl.href;
41
- } catch {
42
- return posterPath;
43
- }
44
- }
45
-
46
- /**
47
- * Capture a frame from the underlying video as a data URL suitable
48
- * for use as `<video>.poster`. Returns `null` when the element is
49
- * not a video, the renderer isn't ready, or the capture fails.
50
- *
51
- * When the control bar has a hidden "preview video" element (used
52
- * for the seek hover thumbnail), we prefer that so we don't disturb
53
- * the user's current playback position.
54
- */
55
- async generateFromVideo(time = 10): Promise<string | null> {
56
- const player = this.player;
57
- if (player.element.tagName !== 'VIDEO') return null;
58
-
59
- const renderer = player.renderer;
60
- if (!renderer || !renderer.media || renderer.media.tagName !== 'VIDEO') {
61
- return null;
62
- }
63
-
64
- const video = renderer.media as HTMLVideoElement;
65
-
66
- if (!video.duration || video.duration < time) {
67
- time = Math.min(time, Math.max(1, video.duration * 0.1));
68
- }
69
-
70
- let videoToUse: HTMLVideoElement = video;
71
- if (player.controlBar && player.controlBar.previewVideo && player.controlBar.previewSupported) {
72
- videoToUse = player.controlBar.previewVideo as HTMLVideoElement;
73
- }
74
-
75
- // If we're using the main video we have to put its currentTime /
76
- // paused state back; for the preview video nobody is watching so
77
- // we can skip the restore.
78
- const restoreState = videoToUse === video;
79
- return await captureVideoFrame(videoToUse, time, {
80
- restoreState,
81
- quality: 0.9
82
- });
83
- }
84
-
85
- /**
86
- * Auto-generate a poster from the video at the 10-second mark if the
87
- * content doesn't already have one. No-op for audio elements and for
88
- * media that ships with a poster attribute or option.
89
- */
90
- async autoGenerate(): Promise<void> {
91
- const player = this.player;
92
-
93
- const hasPoster =
94
- player.element.getAttribute('poster') ||
95
- (player.element as HTMLVideoElement).poster ||
96
- player.options.poster;
97
-
98
- if (hasPoster) return;
99
- if (player.element.tagName !== 'VIDEO') return;
100
-
101
- // Wait for metadata so `duration` is known before we seek.
102
- if (!player.state.duration || player.state.duration === 0) {
103
- await new Promise<void>((resolve) => {
104
- const onLoadedMetadata = () => {
105
- player.element.removeEventListener('loadedmetadata', onLoadedMetadata);
106
- resolve();
107
- };
108
-
109
- if (player.element.readyState >= 1) {
110
- resolve();
111
- } else {
112
- player.element.addEventListener('loadedmetadata', onLoadedMetadata);
113
- }
114
- });
115
- }
116
-
117
- const posterDataURL = await this.generateFromVideo(10);
118
-
119
- if (posterDataURL) {
120
- (player.element as HTMLVideoElement).poster = posterDataURL;
121
- player.log('Auto-generated poster from video frame at 10 seconds', 'info');
122
- this.showOverlay();
123
- }
124
- }
125
-
126
- /**
127
- * Apply the poster as a CSS background on the video wrapper. This is
128
- * used to keep the poster visible behind the play button when the
129
- * browser wouldn't render `<video>.poster` itself (e.g. during
130
- * fallback / transitional states).
131
- */
132
- showOverlay(): void {
133
- const player = this.player;
134
- if (!player.videoWrapper || player.element.tagName !== 'VIDEO') return;
135
-
136
- const poster =
137
- player.element.getAttribute('poster') ||
138
- (player.element as HTMLVideoElement).poster ||
139
- player.options.poster;
140
-
141
- if (!poster) return;
142
-
143
- // Data URLs are already absolute and opaque; only normal URLs need
144
- // resolving into absolute form for the CSS `url(...)` value.
145
- const resolvedPoster = poster.startsWith('data:') ? poster : this.resolvePath(poster);
146
- player.videoWrapper.style.setProperty('--vidply-poster-image', `url("${resolvedPoster}")`);
147
- player.videoWrapper.classList.add('vidply-forced-poster');
148
-
149
- // Audio-as-video content uses a wider aspect ratio so the poster
150
- // doesn't get stretched into a tall letter-boxed frame.
151
- if (player._isAudioContent && player.container) {
152
- player.container.classList.add('vidply-audio-content');
153
- } else if (player.container) {
154
- player.container.classList.remove('vidply-audio-content');
155
- }
156
- }
157
-
158
- hideOverlay(): void {
159
- const player = this.player;
160
- if (!player.videoWrapper) return;
161
-
162
- player.videoWrapper.classList.remove('vidply-forced-poster');
163
- player.videoWrapper.style.removeProperty('--vidply-poster-image');
164
-
165
- // NOTE: `vidply-audio-content` intentionally stays applied — it
166
- // drives the aspect ratio for the entire duration of audio-in-a-
167
- // video-element playback, not just while the poster is visible.
168
- }
169
- }
1
+ /**
2
+ * Poster / artwork helpers lifted out of Player.
3
+ *
4
+ * The manager owns:
5
+ * - relative -> absolute URL resolution;
6
+ * - the canvas-based "grab a frame from the video as poster" logic;
7
+ * - the `.vidply-forced-poster` overlay that shows the poster while
8
+ * the underlying `<video>` is in a paused / not-yet-loaded state.
9
+ *
10
+ * Player keeps public delegating methods (`resolvePosterPath`,
11
+ * `showPosterOverlay`, etc.) so the existing external API is
12
+ * unchanged — only the implementation has moved.
13
+ */
14
+
15
+ import { captureVideoFrame } from '../utils/VideoFrameCapture.js';
16
+ import type { Player } from './Player.js';
17
+
18
+ export class PosterManager {
19
+ private readonly player: Player;
20
+
21
+ constructor(player: Player) {
22
+ this.player = player;
23
+ }
24
+
25
+ /**
26
+ * Convert a relative poster path into an absolute URL. Absolute URLs
27
+ * (http/https) and root-relative paths (`/foo`) are returned as-is.
28
+ * Falls back to the raw string on any parse error — a malformed URL
29
+ * is still better than throwing and breaking the caller.
30
+ */
31
+ resolvePath(posterPath: string | null | undefined): string {
32
+ if (!posterPath) return '';
33
+
34
+ if (posterPath.match(/^(https?:|\/)/)) {
35
+ return posterPath;
36
+ }
37
+
38
+ try {
39
+ const posterUrl = new URL(posterPath, window.location.href);
40
+ return posterUrl.href;
41
+ } catch {
42
+ return posterPath;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Capture a frame from the underlying video as a data URL suitable
48
+ * for use as `<video>.poster`. Returns `null` when the element is
49
+ * not a video, the renderer isn't ready, or the capture fails.
50
+ *
51
+ * When the control bar has a hidden "preview video" element (used
52
+ * for the seek hover thumbnail), we prefer that so we don't disturb
53
+ * the user's current playback position.
54
+ */
55
+ async generateFromVideo(time = 10): Promise<string | null> {
56
+ const player = this.player;
57
+ if (player.element.tagName !== 'VIDEO') return null;
58
+
59
+ const renderer = player.renderer;
60
+ if (!renderer || !renderer.media || renderer.media.tagName !== 'VIDEO') {
61
+ return null;
62
+ }
63
+
64
+ const video = renderer.media as HTMLVideoElement;
65
+
66
+ if (!video.duration || video.duration < time) {
67
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
68
+ }
69
+
70
+ let videoToUse: HTMLVideoElement = video;
71
+ if (player.controlBar && player.controlBar.previewVideo && player.controlBar.previewSupported) {
72
+ videoToUse = player.controlBar.previewVideo as HTMLVideoElement;
73
+ }
74
+
75
+ // If we're using the main video we have to put its currentTime /
76
+ // paused state back; for the preview video nobody is watching so
77
+ // we can skip the restore.
78
+ const restoreState = videoToUse === video;
79
+ return await captureVideoFrame(videoToUse, time, {
80
+ restoreState,
81
+ quality: 0.9
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Auto-generate a poster from the video at the 10-second mark if the
87
+ * content doesn't already have one. No-op for audio elements and for
88
+ * media that ships with a poster attribute or option.
89
+ */
90
+ async autoGenerate(): Promise<void> {
91
+ const player = this.player;
92
+
93
+ const hasPoster =
94
+ player.element.getAttribute('poster') ||
95
+ (player.element as HTMLVideoElement).poster ||
96
+ player.options.poster;
97
+
98
+ if (hasPoster) return;
99
+ if (player.element.tagName !== 'VIDEO') return;
100
+
101
+ // Wait for metadata so `duration` is known before we seek.
102
+ if (!player.state.duration || player.state.duration === 0) {
103
+ await new Promise<void>((resolve) => {
104
+ const onLoadedMetadata = () => {
105
+ player.element.removeEventListener('loadedmetadata', onLoadedMetadata);
106
+ resolve();
107
+ };
108
+
109
+ if (player.element.readyState >= 1) {
110
+ resolve();
111
+ } else {
112
+ player.element.addEventListener('loadedmetadata', onLoadedMetadata);
113
+ }
114
+ });
115
+ }
116
+
117
+ const posterDataURL = await this.generateFromVideo(10);
118
+
119
+ if (posterDataURL) {
120
+ (player.element as HTMLVideoElement).poster = posterDataURL;
121
+ player.log('Auto-generated poster from video frame at 10 seconds', 'info');
122
+ this.showOverlay();
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Apply the poster as a CSS background on the video wrapper. This is
128
+ * used to keep the poster visible behind the play button when the
129
+ * browser wouldn't render `<video>.poster` itself (e.g. during
130
+ * fallback / transitional states).
131
+ */
132
+ showOverlay(): void {
133
+ const player = this.player;
134
+ if (!player.videoWrapper || player.element.tagName !== 'VIDEO') return;
135
+
136
+ const poster =
137
+ player.element.getAttribute('poster') ||
138
+ (player.element as HTMLVideoElement).poster ||
139
+ player.options.poster;
140
+
141
+ if (!poster) return;
142
+
143
+ // Data URLs are already absolute and opaque; only normal URLs need
144
+ // resolving into absolute form for the CSS `url(...)` value.
145
+ const resolvedPoster = poster.startsWith('data:') ? poster : this.resolvePath(poster);
146
+ player.videoWrapper.style.setProperty('--vidply-poster-image', `url("${resolvedPoster}")`);
147
+ player.videoWrapper.classList.add('vidply-forced-poster');
148
+
149
+ // Audio-as-video content uses a wider aspect ratio so the poster
150
+ // doesn't get stretched into a tall letter-boxed frame.
151
+ if (player._isAudioContent && player.container) {
152
+ player.container.classList.add('vidply-audio-content');
153
+ } else if (player.container) {
154
+ player.container.classList.remove('vidply-audio-content');
155
+ }
156
+ }
157
+
158
+ hideOverlay(): void {
159
+ const player = this.player;
160
+ if (!player.videoWrapper) return;
161
+
162
+ player.videoWrapper.classList.remove('vidply-forced-poster');
163
+ player.videoWrapper.style.removeProperty('--vidply-poster-image');
164
+
165
+ // NOTE: `vidply-audio-content` intentionally stays applied — it
166
+ // drives the aspect ratio for the entire duration of audio-in-a-
167
+ // video-element playback, not just while the poster is visible.
168
+ }
169
+ }