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,290 +1,290 @@
|
|
|
1
|
-
import type { Renderer } from '../types/renderer.js';
|
|
2
|
-
import type { Player } from '../core/Player.js';
|
|
3
|
-
import { loadScriptOnce } from '../utils/ScriptLoader.js';
|
|
4
|
-
|
|
5
|
-
export class YouTubeRenderer implements Renderer {
|
|
6
|
-
player: Player;
|
|
7
|
-
media: HTMLMediaElement;
|
|
8
|
-
youtube: YTPlayer | null;
|
|
9
|
-
videoId: string | null;
|
|
10
|
-
isReady: boolean;
|
|
11
|
-
iframe: HTMLDivElement | null;
|
|
12
|
-
timeUpdateInterval?: ReturnType<typeof setInterval>;
|
|
13
|
-
|
|
14
|
-
constructor(player: Player) {
|
|
15
|
-
this.player = player;
|
|
16
|
-
this.media = player.element;
|
|
17
|
-
this.youtube = null;
|
|
18
|
-
this.videoId = null;
|
|
19
|
-
this.isReady = false;
|
|
20
|
-
this.iframe = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async init() {
|
|
24
|
-
// Extract video ID from URL - use currentSource which works for external renderers
|
|
25
|
-
const src = this.player.currentSource || this.player.element.src;
|
|
26
|
-
this.videoId = this.extractVideoId(src);
|
|
27
|
-
|
|
28
|
-
if (!this.videoId) {
|
|
29
|
-
throw new Error('Invalid YouTube URL');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Load YouTube IFrame API
|
|
33
|
-
await this.loadYouTubeAPI();
|
|
34
|
-
|
|
35
|
-
// Create iframe
|
|
36
|
-
this.createIframe();
|
|
37
|
-
|
|
38
|
-
// Initialize player
|
|
39
|
-
await this.initializePlayer();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
extractVideoId(url: string) {
|
|
43
|
-
const patterns = [
|
|
44
|
-
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/,
|
|
45
|
-
/youtube\.com\/embed\/([^&\s]+)/
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
for (const pattern of patterns) {
|
|
49
|
-
const match = url.match(pattern);
|
|
50
|
-
if (match && match[1]) {
|
|
51
|
-
return match[1];
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async loadYouTubeAPI() {
|
|
59
|
-
// Check if API is already loaded
|
|
60
|
-
if (window.YT && window.YT.Player) {
|
|
61
|
-
return Promise.resolve();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// The IFrame API exposes window.YT.Player a short time after its script
|
|
65
|
-
// loads (it signals readiness via the global onYouTubeIframeAPIReady
|
|
66
|
-
// callback). loadScriptOnce dedupes the injection and the readiness
|
|
67
|
-
// predicate polls for YT.Player, also covering the case where the host
|
|
68
|
-
// page already included the API script.
|
|
69
|
-
return loadScriptOnce('https://www.youtube.com/iframe_api', {
|
|
70
|
-
isReady: () => !!(window.YT && window.YT.Player),
|
|
71
|
-
readyTimeout: 8000
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
createIframe() {
|
|
76
|
-
// Hide original element
|
|
77
|
-
this.player.element.style.display = 'none';
|
|
78
|
-
|
|
79
|
-
// Create container for iframe
|
|
80
|
-
this.iframe = document.createElement('div');
|
|
81
|
-
this.iframe.id = `youtube-player-${Math.random().toString(36).substr(2, 9)}`;
|
|
82
|
-
this.iframe.style.width = '100%';
|
|
83
|
-
this.iframe.style.maxHeight = '100%';
|
|
84
|
-
|
|
85
|
-
this.player.element.parentNode?.insertBefore(this.iframe, this.player.element);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async initializePlayer() {
|
|
89
|
-
return new Promise<void>((resolve, reject) => {
|
|
90
|
-
if (!window.YT || !this.iframe) {
|
|
91
|
-
// The SDK never became available (or the iframe container is missing).
|
|
92
|
-
// Reject so Player.init() surfaces a real failure instead of treating
|
|
93
|
-
// a non-functional embed as a successful initialization.
|
|
94
|
-
reject(new Error('YouTube IFrame API is not available'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
this.youtube = new window.YT.Player(this.iframe.id, {
|
|
98
|
-
videoId: this.videoId ?? undefined,
|
|
99
|
-
width: '100%',
|
|
100
|
-
height: '100%',
|
|
101
|
-
playerVars: {
|
|
102
|
-
controls: 1, // Use YouTube native controls
|
|
103
|
-
disablekb: 0, // Allow keyboard controls
|
|
104
|
-
fs: 1, // Allow fullscreen
|
|
105
|
-
modestbranding: 1,
|
|
106
|
-
rel: 0,
|
|
107
|
-
showinfo: 0,
|
|
108
|
-
iv_load_policy: 3,
|
|
109
|
-
autoplay: this.player.options.autoplay ? 1 : 0,
|
|
110
|
-
mute: this.player.options.muted ? 1 : 0,
|
|
111
|
-
start: this.player.options.startTime || 0
|
|
112
|
-
},
|
|
113
|
-
events: {
|
|
114
|
-
onReady: (_event: unknown) => {
|
|
115
|
-
this.isReady = true;
|
|
116
|
-
this.attachEvents();
|
|
117
|
-
// Hide VidPly controls - YouTube has its own
|
|
118
|
-
if (this.player.container) {
|
|
119
|
-
this.player.container.classList.add('vidply-external-controls');
|
|
120
|
-
}
|
|
121
|
-
resolve();
|
|
122
|
-
},
|
|
123
|
-
onStateChange: (event: unknown) => this.handleStateChange(event as { data: number }),
|
|
124
|
-
onError: (event: unknown) => this.handleError(event as { data: 2 | 5 | 100 | 101 | 150 })
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
attachEvents() {
|
|
131
|
-
// Set up polling for time updates (YouTube doesn't provide timeupdate events)
|
|
132
|
-
this.timeUpdateInterval = setInterval(() => {
|
|
133
|
-
const youtube = this.youtube;
|
|
134
|
-
if (this.isReady && youtube) {
|
|
135
|
-
const currentTime = youtube.getCurrentTime();
|
|
136
|
-
const duration = youtube.getDuration();
|
|
137
|
-
|
|
138
|
-
this.player.state.currentTime = currentTime;
|
|
139
|
-
this.player.state.duration = duration;
|
|
140
|
-
|
|
141
|
-
this.player.emit('timeupdate', currentTime);
|
|
142
|
-
}
|
|
143
|
-
}, 250);
|
|
144
|
-
|
|
145
|
-
// Initial metadata
|
|
146
|
-
const youtube = this.youtube;
|
|
147
|
-
if (youtube && youtube.getDuration) {
|
|
148
|
-
this.player.state.duration = youtube.getDuration();
|
|
149
|
-
this.player.emit('loadedmetadata');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
handleStateChange(event: { data: number }) {
|
|
154
|
-
const states = window.YT?.PlayerState;
|
|
155
|
-
if (!states) return;
|
|
156
|
-
|
|
157
|
-
switch (event.data) {
|
|
158
|
-
case states.PLAYING:
|
|
159
|
-
this.player.state.playing = true;
|
|
160
|
-
this.player.state.paused = false;
|
|
161
|
-
this.player.state.ended = false;
|
|
162
|
-
this.player.state.buffering = false;
|
|
163
|
-
this.player.emit('play');
|
|
164
|
-
this.player.emit('playing');
|
|
165
|
-
|
|
166
|
-
if (this.player.options.onPlay) {
|
|
167
|
-
this.player.options.onPlay.call(this.player);
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
|
|
171
|
-
case states.PAUSED:
|
|
172
|
-
this.player.state.playing = false;
|
|
173
|
-
this.player.state.paused = true;
|
|
174
|
-
this.player.emit('pause');
|
|
175
|
-
|
|
176
|
-
if (this.player.options.onPause) {
|
|
177
|
-
this.player.options.onPause.call(this.player);
|
|
178
|
-
}
|
|
179
|
-
break;
|
|
180
|
-
|
|
181
|
-
case states.ENDED:
|
|
182
|
-
this.player.state.playing = false;
|
|
183
|
-
this.player.state.paused = true;
|
|
184
|
-
this.player.state.ended = true;
|
|
185
|
-
this.player.emit('ended');
|
|
186
|
-
|
|
187
|
-
if (this.player.options.onEnded) {
|
|
188
|
-
this.player.options.onEnded.call(this.player);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (this.player.options.loop && this.youtube) {
|
|
192
|
-
this.youtube.seekTo(0);
|
|
193
|
-
this.youtube.playVideo();
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
|
|
197
|
-
case states.BUFFERING:
|
|
198
|
-
this.player.state.buffering = true;
|
|
199
|
-
this.player.emit('waiting');
|
|
200
|
-
break;
|
|
201
|
-
|
|
202
|
-
case states.CUED:
|
|
203
|
-
this.player.emit('loadedmetadata');
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
handleError(event: { data: 2 | 5 | 100 | 101 | 150 }) {
|
|
209
|
-
const errors = {
|
|
210
|
-
2: 'Invalid video ID',
|
|
211
|
-
5: 'HTML5 player error',
|
|
212
|
-
100: 'Video not found',
|
|
213
|
-
101: 'Video not allowed to be played in embedded players',
|
|
214
|
-
150: 'Video not allowed to be played in embedded players'
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const error = new Error(errors[event.data] || 'YouTube player error');
|
|
218
|
-
this.player.handleError(error);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
play() {
|
|
222
|
-
if (this.isReady && this.youtube) {
|
|
223
|
-
// Save scroll position to prevent browser from scrolling to video
|
|
224
|
-
const scrollX = window.scrollX;
|
|
225
|
-
const scrollY = window.scrollY;
|
|
226
|
-
|
|
227
|
-
this.youtube.playVideo();
|
|
228
|
-
|
|
229
|
-
// Restore scroll position immediately to prevent auto-scroll
|
|
230
|
-
window.scrollTo(scrollX, scrollY);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
pause() {
|
|
235
|
-
if (this.isReady && this.youtube) {
|
|
236
|
-
this.youtube.pauseVideo();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
seek(time: number) {
|
|
241
|
-
if (this.isReady && this.youtube) {
|
|
242
|
-
this.youtube.seekTo(time, true);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
setVolume(volume: number) {
|
|
247
|
-
if (this.isReady && this.youtube) {
|
|
248
|
-
this.youtube.setVolume(volume * 100);
|
|
249
|
-
this.player.state.volume = volume;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
setMuted(muted: boolean) {
|
|
254
|
-
if (this.isReady && this.youtube) {
|
|
255
|
-
if (muted) {
|
|
256
|
-
this.youtube.mute();
|
|
257
|
-
} else {
|
|
258
|
-
this.youtube.unMute();
|
|
259
|
-
}
|
|
260
|
-
this.player.state.muted = muted;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
setPlaybackSpeed(speed: number) {
|
|
265
|
-
if (this.isReady && this.youtube) {
|
|
266
|
-
this.youtube.setPlaybackRate(speed);
|
|
267
|
-
this.player.state.playbackSpeed = speed;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
destroy() {
|
|
272
|
-
if (this.timeUpdateInterval) {
|
|
273
|
-
clearInterval(this.timeUpdateInterval);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (this.youtube && this.youtube.destroy) {
|
|
277
|
-
this.youtube.destroy();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (this.iframe && this.iframe.parentNode) {
|
|
281
|
-
this.iframe.parentNode.removeChild(this.iframe);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Show original element
|
|
285
|
-
if (this.player.element) {
|
|
286
|
-
this.player.element.style.display = '';
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
1
|
+
import type { Renderer } from '../types/renderer.js';
|
|
2
|
+
import type { Player } from '../core/Player.js';
|
|
3
|
+
import { loadScriptOnce } from '../utils/ScriptLoader.js';
|
|
4
|
+
|
|
5
|
+
export class YouTubeRenderer implements Renderer {
|
|
6
|
+
player: Player;
|
|
7
|
+
media: HTMLMediaElement;
|
|
8
|
+
youtube: YTPlayer | null;
|
|
9
|
+
videoId: string | null;
|
|
10
|
+
isReady: boolean;
|
|
11
|
+
iframe: HTMLDivElement | null;
|
|
12
|
+
timeUpdateInterval?: ReturnType<typeof setInterval>;
|
|
13
|
+
|
|
14
|
+
constructor(player: Player) {
|
|
15
|
+
this.player = player;
|
|
16
|
+
this.media = player.element;
|
|
17
|
+
this.youtube = null;
|
|
18
|
+
this.videoId = null;
|
|
19
|
+
this.isReady = false;
|
|
20
|
+
this.iframe = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async init() {
|
|
24
|
+
// Extract video ID from URL - use currentSource which works for external renderers
|
|
25
|
+
const src = this.player.currentSource || this.player.element.src;
|
|
26
|
+
this.videoId = this.extractVideoId(src);
|
|
27
|
+
|
|
28
|
+
if (!this.videoId) {
|
|
29
|
+
throw new Error('Invalid YouTube URL');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load YouTube IFrame API
|
|
33
|
+
await this.loadYouTubeAPI();
|
|
34
|
+
|
|
35
|
+
// Create iframe
|
|
36
|
+
this.createIframe();
|
|
37
|
+
|
|
38
|
+
// Initialize player
|
|
39
|
+
await this.initializePlayer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
extractVideoId(url: string) {
|
|
43
|
+
const patterns = [
|
|
44
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/,
|
|
45
|
+
/youtube\.com\/embed\/([^&\s]+)/
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const pattern of patterns) {
|
|
49
|
+
const match = url.match(pattern);
|
|
50
|
+
if (match && match[1]) {
|
|
51
|
+
return match[1];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async loadYouTubeAPI() {
|
|
59
|
+
// Check if API is already loaded
|
|
60
|
+
if (window.YT && window.YT.Player) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The IFrame API exposes window.YT.Player a short time after its script
|
|
65
|
+
// loads (it signals readiness via the global onYouTubeIframeAPIReady
|
|
66
|
+
// callback). loadScriptOnce dedupes the injection and the readiness
|
|
67
|
+
// predicate polls for YT.Player, also covering the case where the host
|
|
68
|
+
// page already included the API script.
|
|
69
|
+
return loadScriptOnce('https://www.youtube.com/iframe_api', {
|
|
70
|
+
isReady: () => !!(window.YT && window.YT.Player),
|
|
71
|
+
readyTimeout: 8000
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
createIframe() {
|
|
76
|
+
// Hide original element
|
|
77
|
+
this.player.element.style.display = 'none';
|
|
78
|
+
|
|
79
|
+
// Create container for iframe
|
|
80
|
+
this.iframe = document.createElement('div');
|
|
81
|
+
this.iframe.id = `youtube-player-${Math.random().toString(36).substr(2, 9)}`;
|
|
82
|
+
this.iframe.style.width = '100%';
|
|
83
|
+
this.iframe.style.maxHeight = '100%';
|
|
84
|
+
|
|
85
|
+
this.player.element.parentNode?.insertBefore(this.iframe, this.player.element);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async initializePlayer() {
|
|
89
|
+
return new Promise<void>((resolve, reject) => {
|
|
90
|
+
if (!window.YT || !this.iframe) {
|
|
91
|
+
// The SDK never became available (or the iframe container is missing).
|
|
92
|
+
// Reject so Player.init() surfaces a real failure instead of treating
|
|
93
|
+
// a non-functional embed as a successful initialization.
|
|
94
|
+
reject(new Error('YouTube IFrame API is not available'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.youtube = new window.YT.Player(this.iframe.id, {
|
|
98
|
+
videoId: this.videoId ?? undefined,
|
|
99
|
+
width: '100%',
|
|
100
|
+
height: '100%',
|
|
101
|
+
playerVars: {
|
|
102
|
+
controls: 1, // Use YouTube native controls
|
|
103
|
+
disablekb: 0, // Allow keyboard controls
|
|
104
|
+
fs: 1, // Allow fullscreen
|
|
105
|
+
modestbranding: 1,
|
|
106
|
+
rel: 0,
|
|
107
|
+
showinfo: 0,
|
|
108
|
+
iv_load_policy: 3,
|
|
109
|
+
autoplay: this.player.options.autoplay ? 1 : 0,
|
|
110
|
+
mute: this.player.options.muted ? 1 : 0,
|
|
111
|
+
start: this.player.options.startTime || 0
|
|
112
|
+
},
|
|
113
|
+
events: {
|
|
114
|
+
onReady: (_event: unknown) => {
|
|
115
|
+
this.isReady = true;
|
|
116
|
+
this.attachEvents();
|
|
117
|
+
// Hide VidPly controls - YouTube has its own
|
|
118
|
+
if (this.player.container) {
|
|
119
|
+
this.player.container.classList.add('vidply-external-controls');
|
|
120
|
+
}
|
|
121
|
+
resolve();
|
|
122
|
+
},
|
|
123
|
+
onStateChange: (event: unknown) => this.handleStateChange(event as { data: number }),
|
|
124
|
+
onError: (event: unknown) => this.handleError(event as { data: 2 | 5 | 100 | 101 | 150 })
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
attachEvents() {
|
|
131
|
+
// Set up polling for time updates (YouTube doesn't provide timeupdate events)
|
|
132
|
+
this.timeUpdateInterval = setInterval(() => {
|
|
133
|
+
const youtube = this.youtube;
|
|
134
|
+
if (this.isReady && youtube) {
|
|
135
|
+
const currentTime = youtube.getCurrentTime();
|
|
136
|
+
const duration = youtube.getDuration();
|
|
137
|
+
|
|
138
|
+
this.player.state.currentTime = currentTime;
|
|
139
|
+
this.player.state.duration = duration;
|
|
140
|
+
|
|
141
|
+
this.player.emit('timeupdate', currentTime);
|
|
142
|
+
}
|
|
143
|
+
}, 250);
|
|
144
|
+
|
|
145
|
+
// Initial metadata
|
|
146
|
+
const youtube = this.youtube;
|
|
147
|
+
if (youtube && youtube.getDuration) {
|
|
148
|
+
this.player.state.duration = youtube.getDuration();
|
|
149
|
+
this.player.emit('loadedmetadata');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
handleStateChange(event: { data: number }) {
|
|
154
|
+
const states = window.YT?.PlayerState;
|
|
155
|
+
if (!states) return;
|
|
156
|
+
|
|
157
|
+
switch (event.data) {
|
|
158
|
+
case states.PLAYING:
|
|
159
|
+
this.player.state.playing = true;
|
|
160
|
+
this.player.state.paused = false;
|
|
161
|
+
this.player.state.ended = false;
|
|
162
|
+
this.player.state.buffering = false;
|
|
163
|
+
this.player.emit('play');
|
|
164
|
+
this.player.emit('playing');
|
|
165
|
+
|
|
166
|
+
if (this.player.options.onPlay) {
|
|
167
|
+
this.player.options.onPlay.call(this.player);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case states.PAUSED:
|
|
172
|
+
this.player.state.playing = false;
|
|
173
|
+
this.player.state.paused = true;
|
|
174
|
+
this.player.emit('pause');
|
|
175
|
+
|
|
176
|
+
if (this.player.options.onPause) {
|
|
177
|
+
this.player.options.onPause.call(this.player);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case states.ENDED:
|
|
182
|
+
this.player.state.playing = false;
|
|
183
|
+
this.player.state.paused = true;
|
|
184
|
+
this.player.state.ended = true;
|
|
185
|
+
this.player.emit('ended');
|
|
186
|
+
|
|
187
|
+
if (this.player.options.onEnded) {
|
|
188
|
+
this.player.options.onEnded.call(this.player);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.player.options.loop && this.youtube) {
|
|
192
|
+
this.youtube.seekTo(0);
|
|
193
|
+
this.youtube.playVideo();
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case states.BUFFERING:
|
|
198
|
+
this.player.state.buffering = true;
|
|
199
|
+
this.player.emit('waiting');
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case states.CUED:
|
|
203
|
+
this.player.emit('loadedmetadata');
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
handleError(event: { data: 2 | 5 | 100 | 101 | 150 }) {
|
|
209
|
+
const errors = {
|
|
210
|
+
2: 'Invalid video ID',
|
|
211
|
+
5: 'HTML5 player error',
|
|
212
|
+
100: 'Video not found',
|
|
213
|
+
101: 'Video not allowed to be played in embedded players',
|
|
214
|
+
150: 'Video not allowed to be played in embedded players'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const error = new Error(errors[event.data] || 'YouTube player error');
|
|
218
|
+
this.player.handleError(error);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
play() {
|
|
222
|
+
if (this.isReady && this.youtube) {
|
|
223
|
+
// Save scroll position to prevent browser from scrolling to video
|
|
224
|
+
const scrollX = window.scrollX;
|
|
225
|
+
const scrollY = window.scrollY;
|
|
226
|
+
|
|
227
|
+
this.youtube.playVideo();
|
|
228
|
+
|
|
229
|
+
// Restore scroll position immediately to prevent auto-scroll
|
|
230
|
+
window.scrollTo(scrollX, scrollY);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pause() {
|
|
235
|
+
if (this.isReady && this.youtube) {
|
|
236
|
+
this.youtube.pauseVideo();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
seek(time: number) {
|
|
241
|
+
if (this.isReady && this.youtube) {
|
|
242
|
+
this.youtube.seekTo(time, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setVolume(volume: number) {
|
|
247
|
+
if (this.isReady && this.youtube) {
|
|
248
|
+
this.youtube.setVolume(volume * 100);
|
|
249
|
+
this.player.state.volume = volume;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
setMuted(muted: boolean) {
|
|
254
|
+
if (this.isReady && this.youtube) {
|
|
255
|
+
if (muted) {
|
|
256
|
+
this.youtube.mute();
|
|
257
|
+
} else {
|
|
258
|
+
this.youtube.unMute();
|
|
259
|
+
}
|
|
260
|
+
this.player.state.muted = muted;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setPlaybackSpeed(speed: number) {
|
|
265
|
+
if (this.isReady && this.youtube) {
|
|
266
|
+
this.youtube.setPlaybackRate(speed);
|
|
267
|
+
this.player.state.playbackSpeed = speed;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
destroy() {
|
|
272
|
+
if (this.timeUpdateInterval) {
|
|
273
|
+
clearInterval(this.timeUpdateInterval);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.youtube && this.youtube.destroy) {
|
|
277
|
+
this.youtube.destroy();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.iframe && this.iframe.parentNode) {
|
|
281
|
+
this.iframe.parentNode.removeChild(this.iframe);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Show original element
|
|
285
|
+
if (this.player.element) {
|
|
286
|
+
this.player.element.style.display = '';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|