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.
- package/README.md +974 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
- package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
- package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
- package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
- package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
- package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
- package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
- package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
- package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
- package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
- package/dist/dev/vidply.esm.js +648 -35
- package/dist/dev/vidply.esm.js.map +3 -3
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
- package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
- package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
- package/dist/legacy/vidply.js +1062 -34
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +127 -55
- package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
- package/dist/vidply.css +5852 -5796
- package/dist/vidply.esm.min.meta.json +204 -149
- package/dist/vidply.min.css +1 -1
- package/package.json +11 -11
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4191 -4159
- package/src/controls/KeyboardHelp.ts +326 -0
- package/src/controls/KeyboardManager.ts +387 -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/MediaSessionManager.ts +341 -0
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2973 -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 +244 -217
- package/src/i18n/languages/en.ts +245 -218
- package/src/i18n/languages/es.ts +241 -214
- package/src/i18n/languages/fr.ts +241 -214
- package/src/i18n/languages/ja.ts +241 -214
- package/src/icons/Icons.ts +168 -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 +5852 -5796
- package/src/types/events.ts +128 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +199 -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-47RCKQF5.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { i18n } from '../i18n/i18n.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Derive a human-readable label for a TextTrack when the source manifest
|
|
5
|
-
* (DASH AdaptationSet, HLS rendition, etc.) didn't provide one. dash.js
|
|
6
|
-
* falls back to the AdaptationSet @id (typically a digit like "2", "3")
|
|
7
|
-
* when no <Label> child element is present, which surfaces as cryptic
|
|
8
|
-
* "2"/"3" entries in the captions menu.
|
|
9
|
-
*
|
|
10
|
-
* dash.js 5.x additionally derives the label as `element.id ?? element.lang`.
|
|
11
|
-
* When neither is present in the MPD, the value is JS `null`, and the
|
|
12
|
-
* browser's TextTrack API stringifies it to the literal "null". We treat
|
|
13
|
-
* "null"/"undefined" (any casing) as placeholders too, and fall back to a
|
|
14
|
-
* localized language name via Intl.DisplayNames.
|
|
15
|
-
*/
|
|
16
|
-
export function deriveTrackLabel(
|
|
17
|
-
rawLabel: string | null | undefined,
|
|
18
|
-
language: string | null | undefined,
|
|
19
|
-
fallbackKey = 'player.captions'
|
|
20
|
-
): string {
|
|
21
|
-
const cleanLabel = (rawLabel ?? '').trim();
|
|
22
|
-
const cleanLang = (language ?? '').trim();
|
|
23
|
-
|
|
24
|
-
const looksLikePlaceholder =
|
|
25
|
-
cleanLabel === '' ||
|
|
26
|
-
/^\d+$/.test(cleanLabel) ||
|
|
27
|
-
/^(null|undefined)$/i.test(cleanLabel);
|
|
28
|
-
if (!looksLikePlaceholder) {
|
|
29
|
-
return cleanLabel;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const cleanLangIsUsable =
|
|
33
|
-
cleanLang !== '' && !/^(null|undefined)$/i.test(cleanLang);
|
|
34
|
-
|
|
35
|
-
if (cleanLangIsUsable) {
|
|
36
|
-
try {
|
|
37
|
-
// Use endonyms: each language shown in its own name
|
|
38
|
-
// ("English", "Deutsch") rather than translated into the UI
|
|
39
|
-
// language ("Englisch", "Deutsch").
|
|
40
|
-
const displayNames = new Intl.DisplayNames([cleanLang, 'en'], { type: 'language' });
|
|
41
|
-
const name = displayNames.of(cleanLang);
|
|
42
|
-
if (name && name.toLowerCase() !== cleanLang.toLowerCase()) {
|
|
43
|
-
return name;
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// Intl.DisplayNames may be unavailable — fall through to the code.
|
|
47
|
-
}
|
|
48
|
-
return cleanLang.toUpperCase();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return i18n.t(fallbackKey);
|
|
52
|
-
}
|
|
1
|
+
import { i18n } from '../i18n/i18n.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a human-readable label for a TextTrack when the source manifest
|
|
5
|
+
* (DASH AdaptationSet, HLS rendition, etc.) didn't provide one. dash.js
|
|
6
|
+
* falls back to the AdaptationSet @id (typically a digit like "2", "3")
|
|
7
|
+
* when no <Label> child element is present, which surfaces as cryptic
|
|
8
|
+
* "2"/"3" entries in the captions menu.
|
|
9
|
+
*
|
|
10
|
+
* dash.js 5.x additionally derives the label as `element.id ?? element.lang`.
|
|
11
|
+
* When neither is present in the MPD, the value is JS `null`, and the
|
|
12
|
+
* browser's TextTrack API stringifies it to the literal "null". We treat
|
|
13
|
+
* "null"/"undefined" (any casing) as placeholders too, and fall back to a
|
|
14
|
+
* localized language name via Intl.DisplayNames.
|
|
15
|
+
*/
|
|
16
|
+
export function deriveTrackLabel(
|
|
17
|
+
rawLabel: string | null | undefined,
|
|
18
|
+
language: string | null | undefined,
|
|
19
|
+
fallbackKey = 'player.captions'
|
|
20
|
+
): string {
|
|
21
|
+
const cleanLabel = (rawLabel ?? '').trim();
|
|
22
|
+
const cleanLang = (language ?? '').trim();
|
|
23
|
+
|
|
24
|
+
const looksLikePlaceholder =
|
|
25
|
+
cleanLabel === '' ||
|
|
26
|
+
/^\d+$/.test(cleanLabel) ||
|
|
27
|
+
/^(null|undefined)$/i.test(cleanLabel);
|
|
28
|
+
if (!looksLikePlaceholder) {
|
|
29
|
+
return cleanLabel;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cleanLangIsUsable =
|
|
33
|
+
cleanLang !== '' && !/^(null|undefined)$/i.test(cleanLang);
|
|
34
|
+
|
|
35
|
+
if (cleanLangIsUsable) {
|
|
36
|
+
try {
|
|
37
|
+
// Use endonyms: each language shown in its own name
|
|
38
|
+
// ("English", "Deutsch") rather than translated into the UI
|
|
39
|
+
// language ("Englisch", "Deutsch").
|
|
40
|
+
const displayNames = new Intl.DisplayNames([cleanLang, 'en'], { type: 'language' });
|
|
41
|
+
const name = displayNames.of(cleanLang);
|
|
42
|
+
if (name && name.toLowerCase() !== cleanLang.toLowerCase()) {
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Intl.DisplayNames may be unavailable — fall through to the code.
|
|
47
|
+
}
|
|
48
|
+
return cleanLang.toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return i18n.t(fallbackKey);
|
|
52
|
+
}
|
package/src/utils/UrlSafe.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* URL sanitisation helpers for values that flow into `<video>.poster`
|
|
3
|
-
* attributes or CSS `url(...)` declarations.
|
|
4
|
-
*
|
|
5
|
-
* Both helpers used to live as file-local functions inside `Player.ts`,
|
|
6
|
-
* which meant `PlaylistManager` interpolated raw `track.poster` values
|
|
7
|
-
* into `backgroundImage` without any validation. They now live here so
|
|
8
|
-
* every consumer of untrusted poster/artwork URLs can share the same
|
|
9
|
-
* allow-list + CSS escape.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Validate a poster/artwork URL before interpolating it into a CSS
|
|
14
|
-
* `url(...)` value or assigning it to `<video>.poster`. Allows `https:`,
|
|
15
|
-
* `http:`, `data:image/<png|jpeg|webp|gif|svg+xml>;...`, root-relative
|
|
16
|
-
* paths starting with `/`, and same-origin relative paths.
|
|
17
|
-
*
|
|
18
|
-
* Returns `null` for anything suspicious — callers must check for null
|
|
19
|
-
* and fall back to a safe default (e.g. skip setting `backgroundImage`).
|
|
20
|
-
*/
|
|
21
|
-
export function sanitizePosterUrl(input: unknown): string | null {
|
|
22
|
-
if (typeof input !== 'string' || input.length === 0 || input.length > 4096) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
const trimmed = input.trim();
|
|
26
|
-
if (!trimmed) return null;
|
|
27
|
-
// Whitespace, quotes, angle brackets and backslashes would let a
|
|
28
|
-
// crafted value break out of a CSS declaration or HTML attribute.
|
|
29
|
-
if (/[\s"'<>\\]/.test(trimmed)) return null;
|
|
30
|
-
|
|
31
|
-
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
32
|
-
return trimmed;
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const url = new URL(trimmed, typeof window !== 'undefined' ? window.location.href : 'http://localhost/');
|
|
36
|
-
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
|
37
|
-
return url.href;
|
|
38
|
-
}
|
|
39
|
-
if (url.protocol === 'data:' && /^data:image\/(png|jpeg|jpg|webp|gif|svg\+xml);/i.test(trimmed)) {
|
|
40
|
-
return trimmed;
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* CSS-escape an already-validated URL for safe interpolation into a
|
|
50
|
-
* `url(...)` value. Defense in depth alongside {@link sanitizePosterUrl}:
|
|
51
|
-
* even if the URL slipped through, the CSS parser cannot leave the
|
|
52
|
-
* `url()` context because `"`, `(`, `)` and `\` are all escaped.
|
|
53
|
-
*/
|
|
54
|
-
export function cssEscapeUrl(url: string): string {
|
|
55
|
-
return url.replace(/["()\\]/g, (m) => `\\${m}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Convenience wrapper: given an unknown input, return a CSS
|
|
60
|
-
* `url("escaped-safe-url")` string ready to assign to
|
|
61
|
-
* `element.style.backgroundImage`, or `null` if the input is unsafe
|
|
62
|
-
* (so the caller can leave the current background untouched instead
|
|
63
|
-
* of clearing it).
|
|
64
|
-
*/
|
|
65
|
-
export function toCssBackgroundImage(input: unknown): string | null {
|
|
66
|
-
const safe = sanitizePosterUrl(input);
|
|
67
|
-
if (!safe) return null;
|
|
68
|
-
return `url("${cssEscapeUrl(safe)}")`;
|
|
69
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* URL sanitisation helpers for values that flow into `<video>.poster`
|
|
3
|
+
* attributes or CSS `url(...)` declarations.
|
|
4
|
+
*
|
|
5
|
+
* Both helpers used to live as file-local functions inside `Player.ts`,
|
|
6
|
+
* which meant `PlaylistManager` interpolated raw `track.poster` values
|
|
7
|
+
* into `backgroundImage` without any validation. They now live here so
|
|
8
|
+
* every consumer of untrusted poster/artwork URLs can share the same
|
|
9
|
+
* allow-list + CSS escape.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a poster/artwork URL before interpolating it into a CSS
|
|
14
|
+
* `url(...)` value or assigning it to `<video>.poster`. Allows `https:`,
|
|
15
|
+
* `http:`, `data:image/<png|jpeg|webp|gif|svg+xml>;...`, root-relative
|
|
16
|
+
* paths starting with `/`, and same-origin relative paths.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` for anything suspicious — callers must check for null
|
|
19
|
+
* and fall back to a safe default (e.g. skip setting `backgroundImage`).
|
|
20
|
+
*/
|
|
21
|
+
export function sanitizePosterUrl(input: unknown): string | null {
|
|
22
|
+
if (typeof input !== 'string' || input.length === 0 || input.length > 4096) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = input.trim();
|
|
26
|
+
if (!trimmed) return null;
|
|
27
|
+
// Whitespace, quotes, angle brackets and backslashes would let a
|
|
28
|
+
// crafted value break out of a CSS declaration or HTML attribute.
|
|
29
|
+
if (/[\s"'<>\\]/.test(trimmed)) return null;
|
|
30
|
+
|
|
31
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(trimmed, typeof window !== 'undefined' ? window.location.href : 'http://localhost/');
|
|
36
|
+
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
|
37
|
+
return url.href;
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol === 'data:' && /^data:image\/(png|jpeg|jpg|webp|gif|svg\+xml);/i.test(trimmed)) {
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* CSS-escape an already-validated URL for safe interpolation into a
|
|
50
|
+
* `url(...)` value. Defense in depth alongside {@link sanitizePosterUrl}:
|
|
51
|
+
* even if the URL slipped through, the CSS parser cannot leave the
|
|
52
|
+
* `url()` context because `"`, `(`, `)` and `\` are all escaped.
|
|
53
|
+
*/
|
|
54
|
+
export function cssEscapeUrl(url: string): string {
|
|
55
|
+
return url.replace(/["()\\]/g, (m) => `\\${m}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convenience wrapper: given an unknown input, return a CSS
|
|
60
|
+
* `url("escaped-safe-url")` string ready to assign to
|
|
61
|
+
* `element.style.backgroundImage`, or `null` if the input is unsafe
|
|
62
|
+
* (so the caller can leave the current background untouched instead
|
|
63
|
+
* of clearing it).
|
|
64
|
+
*/
|
|
65
|
+
export function toCssBackgroundImage(input: unknown): string | null {
|
|
66
|
+
const safe = sanitizePosterUrl(input);
|
|
67
|
+
if (!safe) return null;
|
|
68
|
+
return `url("${cssEscapeUrl(safe)}")`;
|
|
69
|
+
}
|
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
export interface CaptureFrameOptions {
|
|
2
|
-
restoreState?: boolean;
|
|
3
|
-
quality?: number;
|
|
4
|
-
maxWidth?: number;
|
|
5
|
-
maxHeight?: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function captureVideoFrame(
|
|
9
|
-
video: HTMLVideoElement | null,
|
|
10
|
-
time: number,
|
|
11
|
-
options: CaptureFrameOptions = {}
|
|
12
|
-
): Promise<string | null> {
|
|
13
|
-
if (!video || video.tagName !== 'VIDEO') {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const { restoreState = true, quality = 0.9, maxWidth, maxHeight } = options;
|
|
18
|
-
|
|
19
|
-
const wasPlaying = !video.paused;
|
|
20
|
-
const originalTime = video.currentTime;
|
|
21
|
-
const originalMuted = video.muted;
|
|
22
|
-
|
|
23
|
-
if (restoreState) {
|
|
24
|
-
video.muted = true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return new Promise<string | null>((resolve) => {
|
|
28
|
-
const captureFrame = () => {
|
|
29
|
-
try {
|
|
30
|
-
let width = video.videoWidth || 640;
|
|
31
|
-
let height = video.videoHeight || 360;
|
|
32
|
-
|
|
33
|
-
if (maxWidth && width > maxWidth) {
|
|
34
|
-
const ratio = maxWidth / width;
|
|
35
|
-
width = maxWidth;
|
|
36
|
-
height = Math.round(height * ratio);
|
|
37
|
-
}
|
|
38
|
-
if (maxHeight && height > maxHeight) {
|
|
39
|
-
const ratio = maxHeight / height;
|
|
40
|
-
height = maxHeight;
|
|
41
|
-
width = Math.round(width * ratio);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const canvas = document.createElement('canvas');
|
|
45
|
-
canvas.width = width;
|
|
46
|
-
canvas.height = height;
|
|
47
|
-
|
|
48
|
-
const ctx = canvas.getContext('2d');
|
|
49
|
-
if (!ctx) {
|
|
50
|
-
resolve(null);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
ctx.drawImage(video, 0, 0, width, height);
|
|
54
|
-
|
|
55
|
-
const dataURL = canvas.toDataURL('image/jpeg', quality);
|
|
56
|
-
|
|
57
|
-
if (restoreState) {
|
|
58
|
-
video.currentTime = originalTime;
|
|
59
|
-
video.muted = originalMuted;
|
|
60
|
-
if (wasPlaying && !video.paused) {
|
|
61
|
-
// Autoplay can be rejected by browser policy. We log at debug
|
|
62
|
-
// level so the noise doesn't reach production consoles, but
|
|
63
|
-
// unexpected errors are still observable.
|
|
64
|
-
video.play().catch((e: unknown) => {
|
|
65
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
66
|
-
console.debug('[VidPly] preview play() rejected:', e);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
resolve(dataURL);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
75
|
-
console.debug('[VidPly] frame capture failed:', e);
|
|
76
|
-
}
|
|
77
|
-
if (restoreState) {
|
|
78
|
-
video.currentTime = originalTime;
|
|
79
|
-
video.muted = originalMuted;
|
|
80
|
-
if (wasPlaying && !video.paused) {
|
|
81
|
-
video.play().catch((err: unknown) => {
|
|
82
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
83
|
-
console.debug('[VidPly] preview play() rejected:', err);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
resolve(null);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const onSeeked = () => {
|
|
93
|
-
video.removeEventListener('seeked', onSeeked);
|
|
94
|
-
requestAnimationFrame(() => {
|
|
95
|
-
requestAnimationFrame(captureFrame);
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const timeDiff = Math.abs(video.currentTime - time);
|
|
100
|
-
if (timeDiff < 0.1 && video.readyState >= 2) {
|
|
101
|
-
captureFrame();
|
|
102
|
-
} else if (video.readyState >= 1) {
|
|
103
|
-
video.addEventListener('seeked', onSeeked);
|
|
104
|
-
video.currentTime = time;
|
|
105
|
-
} else {
|
|
106
|
-
const onLoadedMetadata = () => {
|
|
107
|
-
video.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
108
|
-
video.addEventListener('seeked', onSeeked);
|
|
109
|
-
video.currentTime = time;
|
|
110
|
-
};
|
|
111
|
-
video.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
1
|
+
export interface CaptureFrameOptions {
|
|
2
|
+
restoreState?: boolean;
|
|
3
|
+
quality?: number;
|
|
4
|
+
maxWidth?: number;
|
|
5
|
+
maxHeight?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function captureVideoFrame(
|
|
9
|
+
video: HTMLVideoElement | null,
|
|
10
|
+
time: number,
|
|
11
|
+
options: CaptureFrameOptions = {}
|
|
12
|
+
): Promise<string | null> {
|
|
13
|
+
if (!video || video.tagName !== 'VIDEO') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { restoreState = true, quality = 0.9, maxWidth, maxHeight } = options;
|
|
18
|
+
|
|
19
|
+
const wasPlaying = !video.paused;
|
|
20
|
+
const originalTime = video.currentTime;
|
|
21
|
+
const originalMuted = video.muted;
|
|
22
|
+
|
|
23
|
+
if (restoreState) {
|
|
24
|
+
video.muted = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new Promise<string | null>((resolve) => {
|
|
28
|
+
const captureFrame = () => {
|
|
29
|
+
try {
|
|
30
|
+
let width = video.videoWidth || 640;
|
|
31
|
+
let height = video.videoHeight || 360;
|
|
32
|
+
|
|
33
|
+
if (maxWidth && width > maxWidth) {
|
|
34
|
+
const ratio = maxWidth / width;
|
|
35
|
+
width = maxWidth;
|
|
36
|
+
height = Math.round(height * ratio);
|
|
37
|
+
}
|
|
38
|
+
if (maxHeight && height > maxHeight) {
|
|
39
|
+
const ratio = maxHeight / height;
|
|
40
|
+
height = maxHeight;
|
|
41
|
+
width = Math.round(width * ratio);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const canvas = document.createElement('canvas');
|
|
45
|
+
canvas.width = width;
|
|
46
|
+
canvas.height = height;
|
|
47
|
+
|
|
48
|
+
const ctx = canvas.getContext('2d');
|
|
49
|
+
if (!ctx) {
|
|
50
|
+
resolve(null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
54
|
+
|
|
55
|
+
const dataURL = canvas.toDataURL('image/jpeg', quality);
|
|
56
|
+
|
|
57
|
+
if (restoreState) {
|
|
58
|
+
video.currentTime = originalTime;
|
|
59
|
+
video.muted = originalMuted;
|
|
60
|
+
if (wasPlaying && !video.paused) {
|
|
61
|
+
// Autoplay can be rejected by browser policy. We log at debug
|
|
62
|
+
// level so the noise doesn't reach production consoles, but
|
|
63
|
+
// unexpected errors are still observable.
|
|
64
|
+
video.play().catch((e: unknown) => {
|
|
65
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
66
|
+
console.debug('[VidPly] preview play() rejected:', e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolve(dataURL);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
75
|
+
console.debug('[VidPly] frame capture failed:', e);
|
|
76
|
+
}
|
|
77
|
+
if (restoreState) {
|
|
78
|
+
video.currentTime = originalTime;
|
|
79
|
+
video.muted = originalMuted;
|
|
80
|
+
if (wasPlaying && !video.paused) {
|
|
81
|
+
video.play().catch((err: unknown) => {
|
|
82
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
83
|
+
console.debug('[VidPly] preview play() rejected:', err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
resolve(null);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onSeeked = () => {
|
|
93
|
+
video.removeEventListener('seeked', onSeeked);
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
requestAnimationFrame(captureFrame);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const timeDiff = Math.abs(video.currentTime - time);
|
|
100
|
+
if (timeDiff < 0.1 && video.readyState >= 2) {
|
|
101
|
+
captureFrame();
|
|
102
|
+
} else if (video.readyState >= 1) {
|
|
103
|
+
video.addEventListener('seeked', onSeeked);
|
|
104
|
+
video.currentTime = time;
|
|
105
|
+
} else {
|
|
106
|
+
const onLoadedMetadata = () => {
|
|
107
|
+
video.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
108
|
+
video.addEventListener('seeked', onSeeked);
|
|
109
|
+
video.currentTime = time;
|
|
110
|
+
};
|
|
111
|
+
video.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|