ts-video-player 0.1.0

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 (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. package/src/ui/index.ts +29 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * ts-video-player YouTube Provider
3
+ *
4
+ * YouTube embed provider using the iframe API.
5
+ *
6
+ * @module providers/youtube
7
+ */
8
+
9
+ import type { ProviderType, Src, YouTubeSource, PlayerOptions, VideoQuality } from '../types'
10
+ import { BaseProvider } from './base'
11
+
12
+ // =============================================================================
13
+ // YouTube URL Parsing
14
+ // =============================================================================
15
+
16
+ /**
17
+ * YouTube URL patterns
18
+ */
19
+ const YOUTUBE_PATTERNS = [
20
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
21
+ /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
22
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
23
+ ]
24
+
25
+ /**
26
+ * Extract YouTube video ID from URL
27
+ */
28
+ export function extractYouTubeId(url: string): string | null {
29
+ for (const pattern of YOUTUBE_PATTERNS) {
30
+ const match = url.match(pattern)
31
+ if (match) return match[1]
32
+ }
33
+ return null
34
+ }
35
+
36
+ /**
37
+ * Check if source is a YouTube source
38
+ */
39
+ export function isYouTubeSource(src: Src): boolean {
40
+ if (typeof src === 'string') {
41
+ return extractYouTubeId(src) !== null
42
+ }
43
+
44
+ if (typeof src === 'object' && 'type' in src) {
45
+ return (src as YouTubeSource).type === 'youtube'
46
+ }
47
+
48
+ return false
49
+ }
50
+
51
+ /**
52
+ * Parse YouTube source
53
+ */
54
+ function parseYouTubeSource(src: Src): { videoId: string; startTime?: number; endTime?: number } | null {
55
+ if (typeof src === 'string') {
56
+ const videoId = extractYouTubeId(src)
57
+ if (!videoId) return null
58
+
59
+ // Parse start/end time from URL params
60
+ const url = new URL(src, 'https://youtube.com')
61
+ const start = url.searchParams.get('t') || url.searchParams.get('start')
62
+ const end = url.searchParams.get('end')
63
+
64
+ return {
65
+ videoId,
66
+ startTime: start ? parseTime(start) : undefined,
67
+ endTime: end ? parseTime(end) : undefined,
68
+ }
69
+ }
70
+
71
+ if (typeof src === 'object' && 'type' in src && (src as YouTubeSource).type === 'youtube') {
72
+ const ytSrc = src as YouTubeSource
73
+ return {
74
+ videoId: ytSrc.videoId || extractYouTubeId(ytSrc.src) || '',
75
+ startTime: ytSrc.startTime,
76
+ endTime: ytSrc.endTime,
77
+ }
78
+ }
79
+
80
+ return null
81
+ }
82
+
83
+ /**
84
+ * Parse time string (e.g., "1m30s", "90", "1:30")
85
+ */
86
+ function parseTime(time: string): number {
87
+ // Already a number in seconds
88
+ if (/^\d+$/.test(time)) return parseInt(time, 10)
89
+
90
+ // Format: 1m30s
91
+ const match1 = time.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/)
92
+ if (match1) {
93
+ const h = parseInt(match1[1] || '0', 10)
94
+ const m = parseInt(match1[2] || '0', 10)
95
+ const s = parseInt(match1[3] || '0', 10)
96
+ return h * 3600 + m * 60 + s
97
+ }
98
+
99
+ // Format: 1:30 or 1:30:00
100
+ const parts = time.split(':').map((p) => parseInt(p, 10))
101
+ if (parts.length === 2) return parts[0] * 60 + parts[1]
102
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]
103
+
104
+ return 0
105
+ }
106
+
107
+ // =============================================================================
108
+ // YouTube API Types
109
+ // =============================================================================
110
+
111
+ interface YTPlayer {
112
+ playVideo(): void
113
+ pauseVideo(): void
114
+ stopVideo(): void
115
+ seekTo(seconds: number, allowSeekAhead: boolean): void
116
+ getCurrentTime(): number
117
+ getDuration(): number
118
+ setVolume(volume: number): void
119
+ getVolume(): number
120
+ mute(): void
121
+ unMute(): void
122
+ isMuted(): boolean
123
+ setPlaybackRate(rate: number): void
124
+ getPlaybackRate(): number
125
+ getAvailablePlaybackRates(): number[]
126
+ setPlaybackQuality(quality: string): void
127
+ getPlaybackQuality(): string
128
+ getAvailableQualityLevels(): string[]
129
+ loadVideoById(options: { videoId: string; startSeconds?: number; endSeconds?: number }): void
130
+ cueVideoById(options: { videoId: string; startSeconds?: number; endSeconds?: number }): void
131
+ getIframe(): HTMLIFrameElement
132
+ getPlayerState(): number
133
+ destroy(): void
134
+ }
135
+
136
+ interface YTPlayerEvent {
137
+ target: YTPlayer
138
+ data: number
139
+ }
140
+
141
+ declare global {
142
+ interface Window {
143
+ YT?: {
144
+ Player: new (
145
+ elementId: string | HTMLElement,
146
+ options: {
147
+ videoId?: string
148
+ width?: number | string
149
+ height?: number | string
150
+ playerVars?: Record<string, string | number>
151
+ events?: {
152
+ onReady?: (event: YTPlayerEvent) => void
153
+ onStateChange?: (event: YTPlayerEvent) => void
154
+ onError?: (event: YTPlayerEvent) => void
155
+ onPlaybackQualityChange?: (event: YTPlayerEvent) => void
156
+ onPlaybackRateChange?: (event: YTPlayerEvent) => void
157
+ }
158
+ },
159
+ ) => YTPlayer
160
+ PlayerState: {
161
+ UNSTARTED: -1
162
+ ENDED: 0
163
+ PLAYING: 1
164
+ PAUSED: 2
165
+ BUFFERING: 3
166
+ CUED: 5
167
+ }
168
+ }
169
+ onYouTubeIframeAPIReady?: () => void
170
+ }
171
+ }
172
+
173
+ // =============================================================================
174
+ // YouTube Provider
175
+ // =============================================================================
176
+
177
+ /**
178
+ * YouTube embed provider
179
+ */
180
+ export class YouTubeProvider extends BaseProvider {
181
+ readonly name = 'youtube'
182
+ readonly type: ProviderType = 'youtube'
183
+
184
+ private player: YTPlayer | null = null
185
+ private iframe: HTMLIFrameElement | null = null
186
+ private pendingVideoId: string | null = null
187
+ private startTime?: number
188
+ private endTime?: number
189
+ private timeUpdateInterval: ReturnType<typeof setInterval> | null = null
190
+ private lastState = -1
191
+
192
+ get mediaElement(): HTMLIFrameElement | null {
193
+ return this.iframe
194
+ }
195
+
196
+ // === Lifecycle ===
197
+
198
+ protected async initialize(): Promise<void> {
199
+ if (!this.container) {
200
+ throw new Error('Container element is required')
201
+ }
202
+
203
+ // Load YouTube API
204
+ await this.loadYouTubeAPI()
205
+
206
+ // Create container div for YouTube player
207
+ const playerDiv = document.createElement('div')
208
+ playerDiv.id = `ts-video-player-yt-${Date.now()}`
209
+ playerDiv.className = 'ts-video-player__youtube'
210
+ this.container.appendChild(playerDiv)
211
+
212
+ // Build player vars
213
+ const playerVars: Record<string, string | number> = {
214
+ autoplay: this.options.autoplay ? 1 : 0,
215
+ controls: 0, // We use our own controls
216
+ disablekb: 1, // Disable keyboard (we handle it)
217
+ enablejsapi: 1,
218
+ iv_load_policy: 3, // Disable annotations
219
+ modestbranding: 1,
220
+ playsinline: this.options.playsinline ? 1 : 0,
221
+ rel: 0, // Don't show related videos
222
+ origin: window.location.origin,
223
+ }
224
+
225
+ // Apply custom YouTube params
226
+ if (this.options.youtubeParams) {
227
+ Object.assign(playerVars, this.options.youtubeParams)
228
+ }
229
+
230
+ // Use privacy-enhanced mode
231
+ playerVars.host = 'https://www.youtube-nocookie.com'
232
+
233
+ // Create player
234
+ this.player = new window.YT!.Player(playerDiv.id, {
235
+ width: '100%',
236
+ height: '100%',
237
+ playerVars,
238
+ events: {
239
+ onReady: () => this.onReady(),
240
+ onStateChange: (e) => this.onStateChange(e),
241
+ onError: (e) => this.onError(e),
242
+ onPlaybackQualityChange: () => this.onQualityChange(),
243
+ onPlaybackRateChange: () => this.onRateChange(),
244
+ },
245
+ })
246
+
247
+ // Get iframe reference
248
+ this.iframe = this.player.getIframe()
249
+ this.iframe.className = 'ts-video-player__media'
250
+ }
251
+
252
+ private async loadYouTubeAPI(): Promise<void> {
253
+ if (window.YT?.Player) return
254
+
255
+ return new Promise((resolve) => {
256
+ // Check if script already loading
257
+ if (document.querySelector('script[src*="youtube.com/iframe_api"]')) {
258
+ const check = () => {
259
+ if (window.YT?.Player) {
260
+ resolve()
261
+ } else {
262
+ setTimeout(check, 100)
263
+ }
264
+ }
265
+ check()
266
+ return
267
+ }
268
+
269
+ // Load script
270
+ const script = document.createElement('script')
271
+ script.src = 'https://www.youtube.com/iframe_api'
272
+ script.async = true
273
+
274
+ window.onYouTubeIframeAPIReady = () => {
275
+ resolve()
276
+ }
277
+
278
+ document.head.appendChild(script)
279
+ })
280
+ }
281
+
282
+ protected cleanup(): void {
283
+ if (this.timeUpdateInterval) {
284
+ clearInterval(this.timeUpdateInterval)
285
+ this.timeUpdateInterval = null
286
+ }
287
+
288
+ this.player?.destroy()
289
+ this.player = null
290
+ this.iframe = null
291
+ }
292
+
293
+ // === Event Handlers ===
294
+
295
+ private onReady(): void {
296
+ // Start time update polling
297
+ this.timeUpdateInterval = setInterval(() => {
298
+ if (this.player && this.lastState === 1) {
299
+ this.events.emit('timeupdate', this.getCurrentTime())
300
+ }
301
+ }, 250)
302
+
303
+ // Emit initial state
304
+ this.events.emit('loadedmetadata')
305
+ this.events.emit('canplay')
306
+
307
+ // Load pending video if any
308
+ if (this.pendingVideoId) {
309
+ this.loadVideo(this.pendingVideoId, this.startTime, this.endTime)
310
+ this.pendingVideoId = null
311
+ }
312
+ }
313
+
314
+ private onStateChange(event: YTPlayerEvent): void {
315
+ const state = event.data
316
+ this.lastState = state
317
+
318
+ const PlayerState = window.YT!.PlayerState
319
+
320
+ switch (state) {
321
+ case PlayerState.UNSTARTED:
322
+ this.events.emit('loadstart')
323
+ break
324
+ case PlayerState.PLAYING:
325
+ this.events.emit('play')
326
+ this.events.emit('playing')
327
+ this.events.emit('statechange', { paused: false, playing: true, waiting: false })
328
+ break
329
+ case PlayerState.PAUSED:
330
+ this.events.emit('pause')
331
+ this.events.emit('statechange', { paused: true, playing: false })
332
+ break
333
+ case PlayerState.BUFFERING:
334
+ this.events.emit('waiting')
335
+ this.events.emit('statechange', { waiting: true })
336
+ break
337
+ case PlayerState.ENDED:
338
+ this.events.emit('ended')
339
+ this.events.emit('statechange', { ended: true, playing: false })
340
+ break
341
+ case PlayerState.CUED:
342
+ this.events.emit('loadeddata')
343
+ this.events.emit('durationchange', this.getDuration())
344
+ break
345
+ }
346
+ }
347
+
348
+ private onError(event: YTPlayerEvent): void {
349
+ const errorMessages: Record<number, string> = {
350
+ 2: 'Invalid video ID',
351
+ 5: 'HTML5 player error',
352
+ 100: 'Video not found or removed',
353
+ 101: 'Video cannot be embedded',
354
+ 150: 'Video cannot be embedded',
355
+ }
356
+
357
+ this.emitError(event.data, errorMessages[event.data] || 'Unknown YouTube error')
358
+ }
359
+
360
+ private onQualityChange(): void {
361
+ const quality = this.player?.getPlaybackQuality() || 'auto'
362
+ const qualities = this.getQualities()
363
+ this.events.emit('qualitychange', qualities.find((q) => q.id === quality) || null)
364
+ }
365
+
366
+ private onRateChange(): void {
367
+ this.events.emit('ratechange', this.getPlaybackRate())
368
+ }
369
+
370
+ // === Loading ===
371
+
372
+ canPlay(src: Src): boolean {
373
+ return isYouTubeSource(src)
374
+ }
375
+
376
+ async load(src: Src): Promise<void> {
377
+ const parsed = parseYouTubeSource(src)
378
+ if (!parsed) {
379
+ this.emitError(2, 'Invalid YouTube source')
380
+ return
381
+ }
382
+
383
+ this.startTime = parsed.startTime
384
+ this.endTime = parsed.endTime
385
+
386
+ if (!this.player || !this._ready) {
387
+ this.pendingVideoId = parsed.videoId
388
+ return
389
+ }
390
+
391
+ this.loadVideo(parsed.videoId, parsed.startTime, parsed.endTime)
392
+ }
393
+
394
+ private loadVideo(videoId: string, startTime?: number, endTime?: number): void {
395
+ if (!this.player) return
396
+
397
+ if (this.options.autoplay) {
398
+ this.player.loadVideoById({
399
+ videoId,
400
+ startSeconds: startTime,
401
+ endSeconds: endTime,
402
+ })
403
+ } else {
404
+ this.player.cueVideoById({
405
+ videoId,
406
+ startSeconds: startTime,
407
+ endSeconds: endTime,
408
+ })
409
+ }
410
+ }
411
+
412
+ // === Playback ===
413
+
414
+ async play(): Promise<void> {
415
+ this.player?.playVideo()
416
+ }
417
+
418
+ pause(): void {
419
+ this.player?.pauseVideo()
420
+ }
421
+
422
+ stop(): void {
423
+ this.player?.stopVideo()
424
+ }
425
+
426
+ // === Seeking ===
427
+
428
+ seekTo(time: number): void {
429
+ this.player?.seekTo(time, true)
430
+ this.events.emit('seeking')
431
+ // YouTube doesn't emit seeked, so we fake it
432
+ setTimeout(() => this.events.emit('seeked'), 100)
433
+ }
434
+
435
+ getCurrentTime(): number {
436
+ return this.player?.getCurrentTime() || 0
437
+ }
438
+
439
+ getDuration(): number {
440
+ return this.player?.getDuration() || 0
441
+ }
442
+
443
+ // === Volume ===
444
+
445
+ setVolume(volume: number): void {
446
+ this.player?.setVolume(Math.round(volume * 100))
447
+ this.events.emit('volumechange', volume, this.getMuted())
448
+ }
449
+
450
+ getVolume(): number {
451
+ return (this.player?.getVolume() || 100) / 100
452
+ }
453
+
454
+ setMuted(muted: boolean): void {
455
+ if (muted) {
456
+ this.player?.mute()
457
+ } else {
458
+ this.player?.unMute()
459
+ }
460
+ this.events.emit('volumechange', this.getVolume(), muted)
461
+ }
462
+
463
+ getMuted(): boolean {
464
+ return this.player?.isMuted() || false
465
+ }
466
+
467
+ // === Playback Rate ===
468
+
469
+ setPlaybackRate(rate: number): void {
470
+ this.player?.setPlaybackRate(rate)
471
+ }
472
+
473
+ getPlaybackRate(): number {
474
+ return this.player?.getPlaybackRate() || 1
475
+ }
476
+
477
+ // === Quality ===
478
+
479
+ getQualities(): VideoQuality[] {
480
+ const levels = this.player?.getAvailableQualityLevels() || []
481
+ const heights: Record<string, number> = {
482
+ highres: 2160,
483
+ hd2160: 2160,
484
+ hd1440: 1440,
485
+ hd1080: 1080,
486
+ hd720: 720,
487
+ large: 480,
488
+ medium: 360,
489
+ small: 240,
490
+ tiny: 144,
491
+ }
492
+
493
+ const current = this.player?.getPlaybackQuality()
494
+
495
+ return levels.map((level) => ({
496
+ id: level,
497
+ width: Math.round((heights[level] || 480) * (16 / 9)),
498
+ height: heights[level] || 480,
499
+ bitrate: 0,
500
+ selected: level === current,
501
+ }))
502
+ }
503
+
504
+ setQuality(quality: VideoQuality | 'auto'): void {
505
+ if (quality === 'auto') {
506
+ this.player?.setPlaybackQuality('default')
507
+ } else {
508
+ this.player?.setPlaybackQuality(quality.id)
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * YouTube Provider Loader
515
+ */
516
+ export const youtubeLoader = {
517
+ name: 'youtube',
518
+ type: 'youtube' as ProviderType,
519
+
520
+ canPlay(src: Src): boolean {
521
+ return isYouTubeSource(src)
522
+ },
523
+
524
+ mediaType(): 'video' {
525
+ return 'video'
526
+ },
527
+
528
+ preconnect(): string[] {
529
+ return [
530
+ 'https://www.youtube-nocookie.com',
531
+ 'https://www.youtube.com',
532
+ 'https://i.ytimg.com',
533
+ 'https://s.ytimg.com',
534
+ ]
535
+ },
536
+
537
+ async load(container: HTMLElement, options: PlayerOptions): Promise<YouTubeProvider> {
538
+ const provider = new YouTubeProvider()
539
+ await provider.setup(container, options)
540
+ return provider
541
+ },
542
+ }