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,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
+