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.
- package/LICENSE.md +21 -0
- package/README.md +438 -0
- package/dist/cdn.d.ts +10 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.min.js +1046 -0
- package/dist/cdn.min.js.map +54 -0
- package/dist/cdn.test.d.ts +2 -0
- package/dist/cdn.test.d.ts.map +1 -0
- package/dist/chunk-36x4yhjn.js +765 -0
- package/dist/chunk-36x4yhjn.js.map +12 -0
- package/dist/chunk-eajts2jv.js +4243 -0
- package/dist/chunk-eajts2jv.js.map +42 -0
- package/dist/chunk-z1ehbz9f.js +380 -0
- package/dist/chunk-z1ehbz9f.js.map +19 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core/audio-gain.d.ts +67 -0
- package/dist/core/audio-gain.d.ts.map +1 -0
- package/dist/core/caption-styles.d.ts +121 -0
- package/dist/core/caption-styles.d.ts.map +1 -0
- package/dist/core/chapters.d.ts +150 -0
- package/dist/core/chapters.d.ts.map +1 -0
- package/dist/core/error-overlay.d.ts +71 -0
- package/dist/core/error-overlay.d.ts.map +1 -0
- package/dist/core/events.d.ts +119 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/features.d.ts +64 -0
- package/dist/core/features.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +132 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/live.d.ts +130 -0
- package/dist/core/live.d.ts.map +1 -0
- package/dist/core/media-session.d.ts +38 -0
- package/dist/core/media-session.d.ts.map +1 -0
- package/dist/core/orientation.d.ts +22 -0
- package/dist/core/orientation.d.ts.map +1 -0
- package/dist/core/popover.d.ts +42 -0
- package/dist/core/popover.d.ts.map +1 -0
- package/dist/core/remote-playback.d.ts +195 -0
- package/dist/core/remote-playback.d.ts.map +1 -0
- package/dist/core/settings-menu.d.ts +132 -0
- package/dist/core/settings-menu.d.ts.map +1 -0
- package/dist/core/spinner.d.ts +67 -0
- package/dist/core/spinner.d.ts.map +1 -0
- package/dist/core/state.d.ts +142 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/text-tracks.d.ts +162 -0
- package/dist/core/text-tracks.d.ts.map +1 -0
- package/dist/core/themes.d.ts +213 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/thumbnails.d.ts +124 -0
- package/dist/core/thumbnails.d.ts.map +1 -0
- package/dist/core/tooltips.d.ts +100 -0
- package/dist/core/tooltips.d.ts.map +1 -0
- package/dist/elements/base.d.ts +3 -0
- package/dist/elements/base.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +55 -0
- package/dist/elements/index.js.map +9 -0
- package/dist/elements/media-airplay-button.d.ts +13 -0
- package/dist/elements/media-airplay-button.d.ts.map +1 -0
- package/dist/elements/media-error-display.d.ts +13 -0
- package/dist/elements/media-error-display.d.ts.map +1 -0
- package/dist/elements/media-fullscreen-button.d.ts +13 -0
- package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
- package/dist/elements/media-live-button.d.ts +13 -0
- package/dist/elements/media-live-button.d.ts.map +1 -0
- package/dist/elements/media-mute-button.d.ts +13 -0
- package/dist/elements/media-mute-button.d.ts.map +1 -0
- package/dist/elements/media-pip-button.d.ts +13 -0
- package/dist/elements/media-pip-button.d.ts.map +1 -0
- package/dist/elements/media-play-button.d.ts +14 -0
- package/dist/elements/media-play-button.d.ts.map +1 -0
- package/dist/elements/media-playback-rate-button.d.ts +10 -0
- package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
- package/dist/elements/media-preview-time.d.ts +18 -0
- package/dist/elements/media-preview-time.d.ts.map +1 -0
- package/dist/elements/media-progress-bar.d.ts +16 -0
- package/dist/elements/media-progress-bar.d.ts.map +1 -0
- package/dist/elements/media-remote-playback-button.d.ts +16 -0
- package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
- package/dist/elements/media-seek-button.d.ts +9 -0
- package/dist/elements/media-seek-button.d.ts.map +1 -0
- package/dist/elements/media-settings-menu.d.ts +32 -0
- package/dist/elements/media-settings-menu.d.ts.map +1 -0
- package/dist/elements/media-time-display.d.ts +21 -0
- package/dist/elements/media-time-display.d.ts.map +1 -0
- package/dist/elements/media-time-group.d.ts +22 -0
- package/dist/elements/media-time-group.d.ts.map +1 -0
- package/dist/elements/media-time-separator.d.ts +13 -0
- package/dist/elements/media-time-separator.d.ts.map +1 -0
- package/dist/elements/media-volume-slider.d.ts +16 -0
- package/dist/elements/media-volume-slider.d.ts.map +1 -0
- package/dist/elements/utils.d.ts +27 -0
- package/dist/elements/utils.d.ts.map +1 -0
- package/dist/elements/video-player.d.ts +23 -0
- package/dist/elements/video-player.d.ts.map +1 -0
- package/dist/elements/video-skin.d.ts +23 -0
- package/dist/elements/video-skin.d.ts.map +1 -0
- package/dist/features/captions.d.ts +8 -0
- package/dist/features/captions.d.ts.map +1 -0
- package/dist/features/create-player.d.ts +45 -0
- package/dist/features/create-player.d.ts.map +1 -0
- package/dist/features/fullscreen.d.ts +8 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/gestures.d.ts +8 -0
- package/dist/features/gestures.d.ts.map +1 -0
- package/dist/features/index.d.ts +26 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +31 -0
- package/dist/features/index.js.map +9 -0
- package/dist/features/keyboard.d.ts +8 -0
- package/dist/features/keyboard.d.ts.map +1 -0
- package/dist/features/pip.d.ts +8 -0
- package/dist/features/pip.d.ts.map +1 -0
- package/dist/features/playback.d.ts +8 -0
- package/dist/features/playback.d.ts.map +1 -0
- package/dist/features/quality.d.ts +8 -0
- package/dist/features/quality.d.ts.map +1 -0
- package/dist/features/types.d.ts +26 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/volume.d.ts +8 -0
- package/dist/features/volume.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8149 -0
- package/dist/index.js.map +31 -0
- package/dist/layouts/index.d.ts +58 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/player.d.ts +90 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/plugins/ads.d.ts +311 -0
- package/dist/plugins/ads.d.ts.map +1 -0
- package/dist/plugins/analytics.d.ts +203 -0
- package/dist/plugins/analytics.d.ts.map +1 -0
- package/dist/plugins/end-screen.d.ts +184 -0
- package/dist/plugins/end-screen.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +20 -0
- package/dist/plugins/index.js.map +9 -0
- package/dist/plugins/skip-segments.d.ts +159 -0
- package/dist/plugins/skip-segments.d.ts.map +1 -0
- package/dist/plugins/watermark.d.ts +157 -0
- package/dist/plugins/watermark.d.ts.map +1 -0
- package/dist/providers/base.d.ts +56 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/dash.d.ts +94 -0
- package/dist/providers/dash.d.ts.map +1 -0
- package/dist/providers/hls.d.ts +182 -0
- package/dist/providers/hls.d.ts.map +1 -0
- package/dist/providers/html5.d.ts +64 -0
- package/dist/providers/html5.d.ts.map +1 -0
- package/dist/providers/index.d.ts +41 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/vimeo.d.ts +157 -0
- package/dist/providers/vimeo.d.ts.map +1 -0
- package/dist/providers/youtube.d.ts +134 -0
- package/dist/providers/youtube.d.ts.map +1 -0
- package/dist/stx/index.d.ts +59 -0
- package/dist/stx/index.d.ts.map +1 -0
- package/dist/stx/index.js +480 -0
- package/dist/stx/index.js.map +11 -0
- package/dist/types.d.ts +724 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +121 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +16 -0
- package/dist/ui/index.js.map +9 -0
- package/package.json +109 -0
- package/src/cdn.test.ts +10 -0
- package/src/cdn.ts +60 -0
- package/src/config.ts +25 -0
- package/src/core/audio-gain.ts +171 -0
- package/src/core/caption-styles.ts +568 -0
- package/src/core/chapters.ts +551 -0
- package/src/core/error-overlay.ts +354 -0
- package/src/core/events.ts +552 -0
- package/src/core/features.ts +287 -0
- package/src/core/i18n.ts +590 -0
- package/src/core/index.ts +249 -0
- package/src/core/live.ts +472 -0
- package/src/core/media-session.ts +137 -0
- package/src/core/orientation.ts +57 -0
- package/src/core/popover.ts +143 -0
- package/src/core/remote-playback.ts +661 -0
- package/src/core/settings-menu.ts +630 -0
- package/src/core/spinner.ts +310 -0
- package/src/core/state.ts +476 -0
- package/src/core/text-tracks.ts +599 -0
- package/src/core/themes.ts +607 -0
- package/src/core/thumbnails.ts +457 -0
- package/src/core/tooltips.ts +370 -0
- package/src/elements/base.ts +2 -0
- package/src/elements/index.ts +82 -0
- package/src/elements/media-airplay-button.ts +78 -0
- package/src/elements/media-error-display.ts +47 -0
- package/src/elements/media-fullscreen-button.ts +72 -0
- package/src/elements/media-live-button.ts +65 -0
- package/src/elements/media-mute-button.ts +69 -0
- package/src/elements/media-pip-button.ts +66 -0
- package/src/elements/media-play-button.ts +69 -0
- package/src/elements/media-playback-rate-button.ts +59 -0
- package/src/elements/media-preview-time.ts +75 -0
- package/src/elements/media-progress-bar.ts +200 -0
- package/src/elements/media-remote-playback-button.ts +108 -0
- package/src/elements/media-seek-button.ts +50 -0
- package/src/elements/media-settings-menu.ts +302 -0
- package/src/elements/media-time-display.ts +99 -0
- package/src/elements/media-time-group.ts +41 -0
- package/src/elements/media-time-separator.ts +35 -0
- package/src/elements/media-volume-slider.ts +140 -0
- package/src/elements/utils.ts +82 -0
- package/src/elements/video-player.ts +175 -0
- package/src/elements/video-skin.ts +127 -0
- package/src/features/captions.ts +56 -0
- package/src/features/create-player.ts +91 -0
- package/src/features/fullscreen.ts +44 -0
- package/src/features/gestures.ts +44 -0
- package/src/features/index.ts +44 -0
- package/src/features/keyboard.ts +89 -0
- package/src/features/pip.ts +47 -0
- package/src/features/playback.ts +68 -0
- package/src/features/quality.ts +19 -0
- package/src/features/types.ts +28 -0
- package/src/features/volume.ts +46 -0
- package/src/index.ts +635 -0
- package/src/layouts/index.ts +672 -0
- package/src/player.ts +821 -0
- package/src/plugins/ads.ts +1045 -0
- package/src/plugins/analytics.ts +673 -0
- package/src/plugins/end-screen.ts +793 -0
- package/src/plugins/index.ts +61 -0
- package/src/plugins/skip-segments.ts +537 -0
- package/src/plugins/watermark.ts +537 -0
- package/src/providers/base.ts +197 -0
- package/src/providers/dash.ts +588 -0
- package/src/providers/hls.ts +624 -0
- package/src/providers/html5.ts +473 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/vimeo.ts +533 -0
- package/src/providers/youtube.ts +542 -0
- package/src/stx/index.ts +606 -0
- package/src/types.ts +874 -0
- package/src/ui/components.ts +910 -0
- 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
|
+
}
|