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,661 @@
1
+ /**
2
+ * Remote Playback (AirPlay & Google Cast)
3
+ *
4
+ * Provides support for remote playback via AirPlay and Google Cast.
5
+ *
6
+ * @module core/remote-playback
7
+ */
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export type RemotePlaybackType = 'airplay' | 'googlecast' | 'none'
14
+
15
+ export type RemotePlaybackState = 'disconnected' | 'connecting' | 'connected'
16
+
17
+ export interface RemoteDevice {
18
+ /** Device name */
19
+ name: string
20
+ /** Device type */
21
+ type: RemotePlaybackType
22
+ /** Device ID (if available) */
23
+ id?: string
24
+ }
25
+
26
+ export interface RemotePlaybackConfig {
27
+ /** Enable AirPlay (default: true on Safari) */
28
+ airPlay?: boolean
29
+ /** Enable Google Cast (default: true) */
30
+ googleCast?: boolean
31
+ /** Google Cast application ID (default: Chrome default receiver) */
32
+ castApplicationId?: string
33
+ /** Callback when device availability changes */
34
+ onAvailabilityChange?: (type: RemotePlaybackType, available: boolean) => void
35
+ /** Callback when connection state changes */
36
+ onStateChange?: (state: RemotePlaybackState, device?: RemoteDevice) => void
37
+ }
38
+
39
+ // =============================================================================
40
+ // AirPlay Support
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Check if AirPlay is supported
45
+ */
46
+ export function isAirPlaySupported(): boolean {
47
+ if (typeof window === 'undefined') return false
48
+ return 'WebKitPlaybackTargetAvailabilityEvent' in window
49
+ }
50
+
51
+ /**
52
+ * Check if an element has AirPlay available
53
+ */
54
+ export function isAirPlayAvailable(video: HTMLVideoElement): boolean {
55
+ return (video as any).webkitCurrentPlaybackTargetIsWireless !== undefined
56
+ }
57
+
58
+ /**
59
+ * Show AirPlay picker
60
+ */
61
+ export function showAirPlayPicker(video: HTMLVideoElement): void {
62
+ if ((video as any).webkitShowPlaybackTargetPicker) {
63
+ (video as any).webkitShowPlaybackTargetPicker()
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check if currently playing via AirPlay
69
+ */
70
+ export function isAirPlaying(video: HTMLVideoElement): boolean {
71
+ return (video as any).webkitCurrentPlaybackTargetIsWireless === true
72
+ }
73
+
74
+ // =============================================================================
75
+ // Google Cast Support
76
+ // =============================================================================
77
+
78
+ declare global {
79
+ interface Window {
80
+ chrome?: {
81
+ cast?: any
82
+ }
83
+ __onGCastApiAvailable?: (isAvailable: boolean) => void
84
+ }
85
+ }
86
+
87
+ let castInitialized = false
88
+ let castSession: any = null
89
+
90
+ /**
91
+ * Check if Google Cast is supported
92
+ */
93
+ export function isGoogleCastSupported(): boolean {
94
+ if (typeof window === 'undefined') return false
95
+ return !!window.chrome?.cast
96
+ }
97
+
98
+ /**
99
+ * Load the Google Cast SDK
100
+ */
101
+ export function loadGoogleCastSDK(): Promise<void> {
102
+ return new Promise((resolve, reject) => {
103
+ if (isGoogleCastSupported()) {
104
+ resolve()
105
+ return
106
+ }
107
+
108
+ // Check if already loading
109
+ if (document.querySelector('script[src*="cast_sender"]')) {
110
+ // Wait for it to load
111
+ window.__onGCastApiAvailable = (isAvailable) => {
112
+ if (isAvailable) {
113
+ resolve()
114
+ }
115
+ else {
116
+ reject(new Error('Google Cast SDK not available'))
117
+ }
118
+ }
119
+ return
120
+ }
121
+
122
+ const script = document.createElement('script')
123
+ script.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'
124
+ script.async = true
125
+
126
+ window.__onGCastApiAvailable = (isAvailable) => {
127
+ if (isAvailable) {
128
+ resolve()
129
+ }
130
+ else {
131
+ reject(new Error('Google Cast SDK not available'))
132
+ }
133
+ }
134
+
135
+ script.onerror = () => reject(new Error('Failed to load Google Cast SDK'))
136
+ document.head.appendChild(script)
137
+ })
138
+ }
139
+
140
+ /**
141
+ * Initialize Google Cast
142
+ */
143
+ export function initializeGoogleCast(applicationId?: string): Promise<void> {
144
+ return new Promise((resolve, reject) => {
145
+ if (!isGoogleCastSupported()) {
146
+ reject(new Error('Google Cast not supported'))
147
+ return
148
+ }
149
+
150
+ if (castInitialized) {
151
+ resolve()
152
+ return
153
+ }
154
+
155
+ const cast = window.chrome!.cast!
156
+ const castContext = cast.framework.CastContext.getInstance()
157
+
158
+ castContext.setOptions({
159
+ receiverApplicationId: applicationId || cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
160
+ autoJoinPolicy: cast.AutoJoinPolicy.ORIGIN_SCOPED,
161
+ })
162
+
163
+ castInitialized = true
164
+ resolve()
165
+ })
166
+ }
167
+
168
+ /**
169
+ * Request a Google Cast session
170
+ */
171
+ export async function requestCastSession(): Promise<any> {
172
+ if (!isGoogleCastSupported()) {
173
+ throw new Error('Google Cast not supported')
174
+ }
175
+
176
+ const cast = window.chrome!.cast!
177
+ const castContext = cast.framework.CastContext.getInstance()
178
+
179
+ try {
180
+ await castContext.requestSession()
181
+ castSession = castContext.getCurrentSession()
182
+ return castSession
183
+ }
184
+ catch (error) {
185
+ throw new Error('Failed to start Cast session')
186
+ }
187
+ }
188
+
189
+ /**
190
+ * End Google Cast session
191
+ */
192
+ export function endCastSession(): void {
193
+ if (castSession) {
194
+ castSession.endSession(true)
195
+ castSession = null
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Check if currently casting
201
+ */
202
+ export function isCasting(): boolean {
203
+ return castSession !== null
204
+ }
205
+
206
+ /**
207
+ * Get current cast session
208
+ */
209
+ export function getCastSession(): any {
210
+ return castSession
211
+ }
212
+
213
+ /**
214
+ * Cast media to connected device
215
+ */
216
+ export function castMedia(
217
+ url: string,
218
+ contentType: string,
219
+ metadata?: {
220
+ title?: string
221
+ subtitle?: string
222
+ images?: string[]
223
+ }
224
+ ): Promise<void> {
225
+ return new Promise((resolve, reject) => {
226
+ if (!castSession) {
227
+ reject(new Error('No active Cast session'))
228
+ return
229
+ }
230
+
231
+ const cast = window.chrome!.cast!
232
+ const mediaInfo = new cast.media.MediaInfo(url, contentType)
233
+
234
+ if (metadata) {
235
+ const meta = new cast.media.GenericMediaMetadata()
236
+ if (metadata.title) meta.title = metadata.title
237
+ if (metadata.subtitle) meta.subtitle = metadata.subtitle
238
+ if (metadata.images) {
239
+ meta.images = metadata.images.map((img: string) => new cast.Image(img))
240
+ }
241
+ mediaInfo.metadata = meta
242
+ }
243
+
244
+ const request = new cast.media.LoadRequest(mediaInfo)
245
+
246
+ castSession.loadMedia(request).then(
247
+ () => resolve(),
248
+ (error: any) => reject(new Error(`Cast failed: ${error.description}`))
249
+ )
250
+ })
251
+ }
252
+
253
+ // =============================================================================
254
+ // Remote Playback Controller
255
+ // =============================================================================
256
+
257
+ export class RemotePlaybackController {
258
+ private video: HTMLVideoElement | null = null
259
+ private config: Required<RemotePlaybackConfig>
260
+ private state: RemotePlaybackState = 'disconnected'
261
+ private connectedDevice: RemoteDevice | null = null
262
+ private airPlayAvailable = false
263
+ private googleCastAvailable = false
264
+ private listeners: Map<string, Set<(...args: any[]) => void>> = new Map()
265
+
266
+ constructor(config: RemotePlaybackConfig = {}) {
267
+ this.config = {
268
+ airPlay: config.airPlay ?? true,
269
+ googleCast: config.googleCast ?? true,
270
+ castApplicationId: config.castApplicationId || '',
271
+ onAvailabilityChange: config.onAvailabilityChange || (() => {}),
272
+ onStateChange: config.onStateChange || (() => {}),
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Attach to a video element
278
+ */
279
+ async attach(video: HTMLVideoElement): Promise<void> {
280
+ this.video = video
281
+
282
+ // Setup AirPlay
283
+ if (this.config.airPlay && isAirPlaySupported()) {
284
+ this.setupAirPlay()
285
+ }
286
+
287
+ // Setup Google Cast
288
+ if (this.config.googleCast) {
289
+ try {
290
+ await loadGoogleCastSDK()
291
+ await initializeGoogleCast(this.config.castApplicationId)
292
+ this.setupGoogleCast()
293
+ }
294
+ catch (error) {
295
+ console.warn('Google Cast initialization failed:', error)
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Setup AirPlay event listeners
302
+ */
303
+ private setupAirPlay(): void {
304
+ if (!this.video) return
305
+
306
+ // Listen for availability
307
+ this.video.addEventListener('webkitplaybacktargetavailabilitychanged', ((event: any) => {
308
+ this.airPlayAvailable = event.availability === 'available'
309
+ this.config.onAvailabilityChange('airplay', this.airPlayAvailable)
310
+ this.emit('airplayavailabilitychange', this.airPlayAvailable)
311
+ }) as EventListener)
312
+
313
+ // Listen for connection state
314
+ this.video.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', (() => {
315
+ const isWireless = isAirPlaying(this.video!)
316
+
317
+ if (isWireless) {
318
+ this.state = 'connected'
319
+ this.connectedDevice = {
320
+ name: 'AirPlay Device',
321
+ type: 'airplay',
322
+ }
323
+ }
324
+ else {
325
+ this.state = 'disconnected'
326
+ this.connectedDevice = null
327
+ }
328
+
329
+ this.config.onStateChange(this.state, this.connectedDevice || undefined)
330
+ this.emit('statechange', this.state, this.connectedDevice)
331
+ }) as EventListener)
332
+ }
333
+
334
+ /**
335
+ * Setup Google Cast event listeners
336
+ */
337
+ private setupGoogleCast(): void {
338
+ if (!isGoogleCastSupported()) return
339
+
340
+ const cast = window.chrome!.cast!
341
+ const castContext = cast.framework.CastContext.getInstance()
342
+
343
+ // Check initial availability
344
+ this.googleCastAvailable = castContext.getCastState() !== cast.framework.CastState.NO_DEVICES_AVAILABLE
345
+ this.config.onAvailabilityChange('googlecast', this.googleCastAvailable)
346
+
347
+ // Listen for cast state changes
348
+ castContext.addEventListener(
349
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
350
+ (event: any) => {
351
+ this.googleCastAvailable = event.castState !== cast.framework.CastState.NO_DEVICES_AVAILABLE
352
+ this.config.onAvailabilityChange('googlecast', this.googleCastAvailable)
353
+ this.emit('googlecastavailabilitychange', this.googleCastAvailable)
354
+
355
+ // Update connection state
356
+ switch (event.castState) {
357
+ case cast.framework.CastState.CONNECTED:
358
+ this.state = 'connected'
359
+ const session = castContext.getCurrentSession()
360
+ this.connectedDevice = {
361
+ name: session?.getCastDevice()?.friendlyName || 'Cast Device',
362
+ type: 'googlecast',
363
+ id: session?.getCastDevice()?.id,
364
+ }
365
+ break
366
+ case cast.framework.CastState.CONNECTING:
367
+ this.state = 'connecting'
368
+ break
369
+ default:
370
+ this.state = 'disconnected'
371
+ this.connectedDevice = null
372
+ }
373
+
374
+ this.config.onStateChange(this.state, this.connectedDevice || undefined)
375
+ this.emit('statechange', this.state, this.connectedDevice)
376
+ }
377
+ )
378
+ }
379
+
380
+ /**
381
+ * Show remote playback picker
382
+ */
383
+ async showPicker(type?: RemotePlaybackType): Promise<void> {
384
+ if (type === 'airplay' || (!type && this.airPlayAvailable)) {
385
+ if (this.video) {
386
+ showAirPlayPicker(this.video)
387
+ }
388
+ }
389
+ else if (type === 'googlecast' || (!type && this.googleCastAvailable)) {
390
+ await requestCastSession()
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Disconnect from remote device
396
+ */
397
+ disconnect(): void {
398
+ if (this.connectedDevice?.type === 'googlecast') {
399
+ endCastSession()
400
+ }
401
+ // AirPlay disconnect is handled by the system
402
+
403
+ this.state = 'disconnected'
404
+ this.connectedDevice = null
405
+ this.emit('statechange', this.state, null)
406
+ }
407
+
408
+ /**
409
+ * Check if AirPlay is available
410
+ */
411
+ isAirPlayAvailable(): boolean {
412
+ return this.airPlayAvailable
413
+ }
414
+
415
+ /**
416
+ * Check if Google Cast is available
417
+ */
418
+ isGoogleCastAvailable(): boolean {
419
+ return this.googleCastAvailable
420
+ }
421
+
422
+ /**
423
+ * Check if any remote playback is available
424
+ */
425
+ isRemotePlaybackAvailable(): boolean {
426
+ return this.airPlayAvailable || this.googleCastAvailable
427
+ }
428
+
429
+ /**
430
+ * Get current state
431
+ */
432
+ getState(): RemotePlaybackState {
433
+ return this.state
434
+ }
435
+
436
+ /**
437
+ * Get connected device
438
+ */
439
+ getConnectedDevice(): RemoteDevice | null {
440
+ return this.connectedDevice
441
+ }
442
+
443
+ /**
444
+ * Check if currently connected to a remote device
445
+ */
446
+ isConnected(): boolean {
447
+ return this.state === 'connected'
448
+ }
449
+
450
+ /**
451
+ * Add event listener
452
+ */
453
+ on(event: string, handler: (...args: any[]) => void): void {
454
+ if (!this.listeners.has(event)) {
455
+ this.listeners.set(event, new Set())
456
+ }
457
+ this.listeners.get(event)!.add(handler)
458
+ }
459
+
460
+ /**
461
+ * Remove event listener
462
+ */
463
+ off(event: string, handler: (...args: any[]) => void): void {
464
+ this.listeners.get(event)?.delete(handler)
465
+ }
466
+
467
+ /**
468
+ * Emit event
469
+ */
470
+ private emit(event: string, ...args: any[]): void {
471
+ this.listeners.get(event)?.forEach(handler => handler(...args))
472
+ }
473
+
474
+ /**
475
+ * Detach from video element
476
+ */
477
+ detach(): void {
478
+ this.disconnect()
479
+ this.video = null
480
+ }
481
+
482
+ /**
483
+ * Destroy controller
484
+ */
485
+ destroy(): void {
486
+ this.detach()
487
+ this.listeners.clear()
488
+ }
489
+ }
490
+
491
+ // =============================================================================
492
+ // UI Components
493
+ // =============================================================================
494
+
495
+ export class AirPlayButton {
496
+ private button: HTMLButtonElement
497
+ private _available = false
498
+
499
+ constructor() {
500
+ this.button = document.createElement('button')
501
+ this.button.className = 'tsvp-airplay-button'
502
+ this.button.type = 'button'
503
+ this.button.setAttribute('aria-label', 'AirPlay')
504
+ this.button.innerHTML = `
505
+ <svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
506
+ <path d="M6 22h12l-6-6-6 6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
507
+ </svg>
508
+ `
509
+
510
+ this.applyStyles()
511
+ this.hide()
512
+ }
513
+
514
+ private applyStyles(): void {
515
+ this.button.style.cssText = `
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ width: var(--tsvp-controls-button-size, 40px);
520
+ height: var(--tsvp-controls-button-size, 40px);
521
+ padding: 0;
522
+ border: none;
523
+ background: transparent;
524
+ color: var(--tsvp-color-text, #ffffff);
525
+ cursor: pointer;
526
+ opacity: 0.8;
527
+ transition: opacity var(--tsvp-transition-normal, 200ms);
528
+ `
529
+ }
530
+
531
+ getElement(): HTMLButtonElement {
532
+ return this.button
533
+ }
534
+
535
+ setAvailable(available: boolean): void {
536
+ this._available = available
537
+ if (available) {
538
+ this.show()
539
+ }
540
+ else {
541
+ this.hide()
542
+ }
543
+ }
544
+
545
+ isAvailable(): boolean {
546
+ return this._available
547
+ }
548
+
549
+ show(): void {
550
+ this.button.style.display = 'flex'
551
+ }
552
+
553
+ hide(): void {
554
+ this.button.style.display = 'none'
555
+ }
556
+
557
+ setActive(active: boolean): void {
558
+ this.button.classList.toggle('tsvp-active', active)
559
+ this.button.style.color = active ? 'var(--tsvp-color-primary, #00a8ff)' : ''
560
+ }
561
+
562
+ onClick(handler: () => void): void {
563
+ this.button.addEventListener('click', handler)
564
+ }
565
+
566
+ destroy(): void {
567
+ this.button.remove()
568
+ }
569
+ }
570
+
571
+ export class GoogleCastButton {
572
+ private button: HTMLButtonElement
573
+ private _available = false
574
+
575
+ constructor() {
576
+ this.button = document.createElement('button')
577
+ this.button.className = 'tsvp-googlecast-button'
578
+ this.button.type = 'button'
579
+ this.button.setAttribute('aria-label', 'Cast')
580
+ this.button.innerHTML = `
581
+ <svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
582
+ <path d="M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11z"/>
583
+ </svg>
584
+ `
585
+
586
+ this.applyStyles()
587
+ this.hide()
588
+ }
589
+
590
+ private applyStyles(): void {
591
+ this.button.style.cssText = `
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ width: var(--tsvp-controls-button-size, 40px);
596
+ height: var(--tsvp-controls-button-size, 40px);
597
+ padding: 0;
598
+ border: none;
599
+ background: transparent;
600
+ color: var(--tsvp-color-text, #ffffff);
601
+ cursor: pointer;
602
+ opacity: 0.8;
603
+ transition: opacity var(--tsvp-transition-normal, 200ms);
604
+ `
605
+ }
606
+
607
+ getElement(): HTMLButtonElement {
608
+ return this.button
609
+ }
610
+
611
+ setAvailable(available: boolean): void {
612
+ this._available = available
613
+ if (available) {
614
+ this.show()
615
+ }
616
+ else {
617
+ this.hide()
618
+ }
619
+ }
620
+
621
+ isAvailable(): boolean {
622
+ return this._available
623
+ }
624
+
625
+ show(): void {
626
+ this.button.style.display = 'flex'
627
+ }
628
+
629
+ hide(): void {
630
+ this.button.style.display = 'none'
631
+ }
632
+
633
+ setActive(active: boolean): void {
634
+ this.button.classList.toggle('tsvp-active', active)
635
+ this.button.style.color = active ? 'var(--tsvp-color-primary, #00a8ff)' : ''
636
+ }
637
+
638
+ onClick(handler: () => void): void {
639
+ this.button.addEventListener('click', handler)
640
+ }
641
+
642
+ destroy(): void {
643
+ this.button.remove()
644
+ }
645
+ }
646
+
647
+ // =============================================================================
648
+ // Factory
649
+ // =============================================================================
650
+
651
+ export function createRemotePlaybackController(config?: RemotePlaybackConfig): RemotePlaybackController {
652
+ return new RemotePlaybackController(config)
653
+ }
654
+
655
+ export function createAirPlayButton(): AirPlayButton {
656
+ return new AirPlayButton()
657
+ }
658
+
659
+ export function createGoogleCastButton(): GoogleCastButton {
660
+ return new GoogleCastButton()
661
+ }