vidply 1.2.0 → 1.2.1
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.
- package/README.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.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
|
+
}
|