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,249 @@
1
+ /**
2
+ * ts-video-player Core
3
+ *
4
+ * Core utilities and systems.
5
+ *
6
+ * @module core
7
+ */
8
+
9
+ // State
10
+ export {
11
+ StateStore,
12
+ createSignal,
13
+ computed,
14
+ createDefaultState,
15
+ loadPersistedState,
16
+ savePersistedState,
17
+ createStorageSync,
18
+ selectBufferedAmount,
19
+ selectProgress,
20
+ selectRemainingTime,
21
+ selectIsIdle,
22
+ selectIsLoading,
23
+ selectIsLive,
24
+ selectIsDVR,
25
+ selectCurrentQuality,
26
+ selectCurrentTextTrack,
27
+ selectCurrentAudioTrack,
28
+ } from './state'
29
+
30
+ // Events
31
+ export {
32
+ EventEmitter,
33
+ MediaEventsNormalizer,
34
+ onFullscreenChange,
35
+ onPiPChange,
36
+ DEFAULT_KEYBOARD_SHORTCUTS,
37
+ createKeyboardHandler,
38
+ createActivityDetector,
39
+ createGestureDetector,
40
+ } from './events'
41
+
42
+ export type { KeyboardActions, GestureHandlers } from './events'
43
+
44
+ // Audio Gain
45
+ export {
46
+ AudioGain,
47
+ createAudioGain,
48
+ GAIN_PRESETS,
49
+ GAIN_STEPS,
50
+ } from './audio-gain'
51
+
52
+ export type { AudioGainAdapter } from './audio-gain'
53
+
54
+ // Text Tracks
55
+ export {
56
+ TextTrackList,
57
+ TextTrackReadyState,
58
+ CaptionRenderer,
59
+ createTextTrackList,
60
+ createCaptionRenderer,
61
+ } from './text-tracks'
62
+
63
+ export type { TextTrackInit, VTTCueInit, CaptionRendererOptions } from './text-tracks'
64
+
65
+ // Themes
66
+ export {
67
+ defaultTheme,
68
+ darkTheme,
69
+ lightTheme,
70
+ minimalTheme,
71
+ netflixTheme,
72
+ youtubeTheme,
73
+ vimeoTheme,
74
+ presetThemes,
75
+ mergeThemes,
76
+ generateThemeCSS,
77
+ createThemeStyleElement,
78
+ applyTheme,
79
+ injectThemeStyles,
80
+ } from './themes'
81
+
82
+ export type {
83
+ Theme,
84
+ ThemeColors,
85
+ ThemeTypography,
86
+ ThemeSpacing,
87
+ ThemeBorderRadius,
88
+ ThemeShadows,
89
+ ThemeTransitions,
90
+ ThemeSlider,
91
+ ThemeControls,
92
+ ThemeMenu,
93
+ ThemeCaptions,
94
+ ThemeThumbnails,
95
+ ThemeLive,
96
+ PresetThemeName,
97
+ } from './themes'
98
+
99
+ // Thumbnails
100
+ export {
101
+ Thumbnails,
102
+ createThumbnails,
103
+ parseThumbnailVTT,
104
+ generateSpriteCues,
105
+ } from './thumbnails'
106
+
107
+ export type {
108
+ ThumbnailSprite,
109
+ ThumbnailCue,
110
+ ThumbnailsConfig,
111
+ } from './thumbnails'
112
+
113
+ // i18n
114
+ export {
115
+ I18n,
116
+ createI18n,
117
+ getI18n,
118
+ setI18n,
119
+ t,
120
+ englishStrings,
121
+ spanishStrings,
122
+ frenchStrings,
123
+ germanStrings,
124
+ japaneseStrings,
125
+ chineseStrings,
126
+ defaultTranslations,
127
+ } from './i18n'
128
+
129
+ export type {
130
+ TranslationStrings,
131
+ TranslationKey,
132
+ I18nConfig,
133
+ } from './i18n'
134
+
135
+ // Live Streaming
136
+ export {
137
+ LiveIndicator,
138
+ SeekToLiveButton,
139
+ LiveController,
140
+ createLiveIndicator,
141
+ createSeekToLiveButton,
142
+ createLiveController,
143
+ } from './live'
144
+
145
+ export type {
146
+ LiveConfig,
147
+ LiveState,
148
+ } from './live'
149
+
150
+ // Remote Playback (AirPlay / Google Cast)
151
+ export {
152
+ RemotePlaybackController,
153
+ AirPlayButton,
154
+ GoogleCastButton,
155
+ createRemotePlaybackController,
156
+ createAirPlayButton,
157
+ createGoogleCastButton,
158
+ isAirPlaySupported,
159
+ isAirPlayAvailable,
160
+ showAirPlayPicker,
161
+ isAirPlaying,
162
+ isGoogleCastSupported,
163
+ loadGoogleCastSDK,
164
+ initializeGoogleCast,
165
+ requestCastSession,
166
+ endCastSession,
167
+ isCasting,
168
+ getCastSession,
169
+ castMedia,
170
+ } from './remote-playback'
171
+
172
+ export type {
173
+ RemotePlaybackType,
174
+ RemotePlaybackState,
175
+ RemoteDevice,
176
+ RemotePlaybackConfig,
177
+ } from './remote-playback'
178
+
179
+ // Chapters
180
+ export {
181
+ ChaptersManager,
182
+ ChapterMenu,
183
+ createChaptersManager,
184
+ createChapterMenu,
185
+ parseChaptersVTT,
186
+ } from './chapters'
187
+
188
+ export type {
189
+ Chapter,
190
+ ChaptersConfig,
191
+ } from './chapters'
192
+
193
+ // Spinner / Buffering Indicator
194
+ export {
195
+ Spinner,
196
+ createSpinner,
197
+ SPINNER_STYLES,
198
+ } from './spinner'
199
+
200
+ export type { SpinnerConfig } from './spinner'
201
+
202
+ // Caption Styling
203
+ export {
204
+ CaptionStyleManager,
205
+ CaptionSettingsMenu,
206
+ createCaptionStyleManager,
207
+ createCaptionSettingsMenu,
208
+ defaultCaptionStyles,
209
+ captionPresets,
210
+ } from './caption-styles'
211
+
212
+ export type {
213
+ CaptionStyleOptions,
214
+ CaptionStylePreset,
215
+ } from './caption-styles'
216
+
217
+ // Tooltips
218
+ export {
219
+ Tooltip,
220
+ TooltipManager,
221
+ createTooltip,
222
+ createTooltipManager,
223
+ TOOLTIP_STYLES,
224
+ } from './tooltips'
225
+
226
+ export type {
227
+ TooltipPlacement,
228
+ TooltipConfig,
229
+ } from './tooltips'
230
+
231
+ // Error Overlay
232
+ export {
233
+ ErrorOverlay,
234
+ createErrorOverlay,
235
+ ERROR_OVERLAY_STYLES,
236
+ } from './error-overlay'
237
+
238
+ export type { ErrorOverlayConfig } from './error-overlay'
239
+
240
+ // Settings Menu
241
+ export {
242
+ SettingsMenu,
243
+ createSettingsMenu,
244
+ } from './settings-menu'
245
+
246
+ export type {
247
+ SettingsMenuConfig,
248
+ SettingsMenuState,
249
+ } from './settings-menu'
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Live Streaming UI
3
+ *
4
+ * Components and utilities for live streaming playback.
5
+ *
6
+ * @module core/live
7
+ */
8
+
9
+ import type { StateStore } from './state'
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ export interface LiveConfig {
16
+ /** Threshold in seconds to consider "at live edge" (default: 10) */
17
+ liveEdgeThreshold?: number
18
+ /** Auto-sync to live when seeking within threshold (default: true) */
19
+ autoSyncToLive?: boolean
20
+ /** Show live indicator badge (default: true) */
21
+ showLiveIndicator?: boolean
22
+ /** Show time behind live (default: true) */
23
+ showTimeBehind?: boolean
24
+ }
25
+
26
+ export interface LiveState {
27
+ /** Whether the stream is live */
28
+ isLive: boolean
29
+ /** Whether currently at the live edge */
30
+ isAtLiveEdge: boolean
31
+ /** Seconds behind the live edge */
32
+ secondsBehindLive: number
33
+ /** Whether DVR (seeking) is available */
34
+ isDVR: boolean
35
+ /** The live edge time (seekable end) */
36
+ liveEdgeTime: number
37
+ }
38
+
39
+ // =============================================================================
40
+ // Live Indicator Component
41
+ // =============================================================================
42
+
43
+ export class LiveIndicator {
44
+ private container: HTMLElement
45
+ private dot: HTMLElement
46
+ private label: HTMLElement
47
+ private timeBehind: HTMLElement
48
+ private state: LiveState = {
49
+ isLive: false,
50
+ isAtLiveEdge: false,
51
+ secondsBehindLive: 0,
52
+ isDVR: false,
53
+ liveEdgeTime: 0,
54
+ }
55
+
56
+ private config: Required<LiveConfig>
57
+
58
+ constructor(config: LiveConfig = {}) {
59
+ this.config = {
60
+ liveEdgeThreshold: config.liveEdgeThreshold ?? 10,
61
+ autoSyncToLive: config.autoSyncToLive ?? true,
62
+ showLiveIndicator: config.showLiveIndicator ?? true,
63
+ showTimeBehind: config.showTimeBehind ?? true,
64
+ }
65
+
66
+ // Create container
67
+ this.container = document.createElement('div')
68
+ this.container.className = 'tsvp-live-indicator'
69
+ this.container.setAttribute('role', 'status')
70
+ this.container.setAttribute('aria-live', 'polite')
71
+
72
+ // Create dot
73
+ this.dot = document.createElement('span')
74
+ this.dot.className = 'tsvp-live-dot'
75
+
76
+ // Create label
77
+ this.label = document.createElement('span')
78
+ this.label.className = 'tsvp-live-label'
79
+ this.label.textContent = 'LIVE'
80
+
81
+ // Create time behind indicator
82
+ this.timeBehind = document.createElement('span')
83
+ this.timeBehind.className = 'tsvp-live-time-behind'
84
+ this.timeBehind.style.display = 'none'
85
+
86
+ this.container.appendChild(this.dot)
87
+ this.container.appendChild(this.label)
88
+ this.container.appendChild(this.timeBehind)
89
+
90
+ this.applyStyles()
91
+ this.hide()
92
+ }
93
+
94
+ /**
95
+ * Apply default styles
96
+ */
97
+ private applyStyles(): void {
98
+ this.container.style.cssText = `
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 6px;
102
+ padding: 4px 10px;
103
+ border-radius: var(--tsvp-radius-full, 9999px);
104
+ background: var(--tsvp-live-badge-bg, #ff0000);
105
+ color: var(--tsvp-live-badge-text, #ffffff);
106
+ font-size: var(--tsvp-font-size-sm, 12px);
107
+ font-weight: var(--tsvp-font-weight-bold, 600);
108
+ font-family: var(--tsvp-font-family, sans-serif);
109
+ cursor: pointer;
110
+ transition: all var(--tsvp-transition-normal, 200ms) var(--tsvp-transition-easing, ease);
111
+ user-select: none;
112
+ `
113
+
114
+ this.dot.style.cssText = `
115
+ width: var(--tsvp-live-indicator-size, 8px);
116
+ height: var(--tsvp-live-indicator-size, 8px);
117
+ border-radius: 50%;
118
+ background: currentColor;
119
+ animation: tsvp-live-pulse 1.5s ease-in-out infinite;
120
+ `
121
+
122
+ this.label.style.cssText = `
123
+ text-transform: uppercase;
124
+ letter-spacing: 0.5px;
125
+ `
126
+
127
+ this.timeBehind.style.cssText = `
128
+ opacity: 0.8;
129
+ font-weight: normal;
130
+ `
131
+
132
+ // Add pulse animation
133
+ if (!document.querySelector('#tsvp-live-styles')) {
134
+ const style = document.createElement('style')
135
+ style.id = 'tsvp-live-styles'
136
+ style.textContent = `
137
+ @keyframes tsvp-live-pulse {
138
+ 0%, 100% { opacity: 1; }
139
+ 50% { opacity: 0.5; }
140
+ }
141
+
142
+ .tsvp-live-indicator.tsvp-live-behind {
143
+ background: rgba(255, 255, 255, 0.2) !important;
144
+ }
145
+
146
+ .tsvp-live-indicator.tsvp-live-behind .tsvp-live-dot {
147
+ animation: none;
148
+ opacity: 0.5;
149
+ }
150
+
151
+ .tsvp-live-indicator:hover {
152
+ transform: scale(1.05);
153
+ }
154
+
155
+ .tsvp-live-indicator:active {
156
+ transform: scale(0.95);
157
+ }
158
+ `
159
+ document.head.appendChild(style)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get the element
165
+ */
166
+ getElement(): HTMLElement {
167
+ return this.container
168
+ }
169
+
170
+ /**
171
+ * Update live state
172
+ */
173
+ update(state: Partial<LiveState>): void {
174
+ this.state = { ...this.state, ...state }
175
+
176
+ if (!this.state.isLive) {
177
+ this.hide()
178
+ return
179
+ }
180
+
181
+ this.show()
182
+
183
+ // Update live edge status
184
+ const isAtEdge = this.state.secondsBehindLive <= this.config.liveEdgeThreshold
185
+ this.state.isAtLiveEdge = isAtEdge
186
+
187
+ if (isAtEdge) {
188
+ this.container.classList.remove('tsvp-live-behind')
189
+ this.label.textContent = 'LIVE'
190
+ this.timeBehind.style.display = 'none'
191
+ this.container.setAttribute('aria-label', 'Live - At live edge')
192
+ }
193
+ else {
194
+ this.container.classList.add('tsvp-live-behind')
195
+
196
+ if (this.config.showTimeBehind && this.state.secondsBehindLive > 0) {
197
+ this.timeBehind.textContent = `-${Math.floor(this.state.secondsBehindLive)}s`
198
+ this.timeBehind.style.display = 'inline'
199
+ }
200
+
201
+ this.container.setAttribute('aria-label', `Live - ${Math.floor(this.state.secondsBehindLive)} seconds behind`)
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Show indicator
207
+ */
208
+ show(): void {
209
+ if (this.config.showLiveIndicator) {
210
+ this.container.style.display = 'inline-flex'
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Hide indicator
216
+ */
217
+ hide(): void {
218
+ this.container.style.display = 'none'
219
+ }
220
+
221
+ /**
222
+ * Set click handler (for seeking to live)
223
+ */
224
+ onClick(handler: () => void): void {
225
+ this.container.addEventListener('click', handler)
226
+ }
227
+
228
+ /**
229
+ * Get current state
230
+ */
231
+ getState(): LiveState {
232
+ return { ...this.state }
233
+ }
234
+
235
+ /**
236
+ * Destroy component
237
+ */
238
+ destroy(): void {
239
+ this.container.remove()
240
+ }
241
+ }
242
+
243
+ // =============================================================================
244
+ // Seek to Live Button
245
+ // =============================================================================
246
+
247
+ export class SeekToLiveButton {
248
+ private button: HTMLButtonElement
249
+ private icon: HTMLSpanElement
250
+ private label: HTMLSpanElement
251
+ private visible = false
252
+
253
+ constructor() {
254
+ this.button = document.createElement('button')
255
+ this.button.className = 'tsvp-seek-live-button'
256
+ this.button.type = 'button'
257
+ this.button.setAttribute('aria-label', 'Seek to live')
258
+
259
+ this.icon = document.createElement('span')
260
+ this.icon.className = 'tsvp-seek-live-icon'
261
+ this.icon.innerHTML = `
262
+ <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
263
+ <path d="M8 5v14l11-7z"/>
264
+ </svg>
265
+ `
266
+
267
+ this.label = document.createElement('span')
268
+ this.label.className = 'tsvp-seek-live-label'
269
+ this.label.textContent = 'Go Live'
270
+
271
+ this.button.appendChild(this.icon)
272
+ this.button.appendChild(this.label)
273
+
274
+ this.applyStyles()
275
+ this.hide()
276
+ }
277
+
278
+ private applyStyles(): void {
279
+ this.button.style.cssText = `
280
+ display: inline-flex;
281
+ align-items: center;
282
+ gap: 4px;
283
+ padding: 6px 12px;
284
+ border: none;
285
+ border-radius: var(--tsvp-radius-full, 9999px);
286
+ background: var(--tsvp-live-badge-bg, #ff0000);
287
+ color: var(--tsvp-live-badge-text, #ffffff);
288
+ font-size: var(--tsvp-font-size-sm, 12px);
289
+ font-weight: var(--tsvp-font-weight-bold, 600);
290
+ font-family: var(--tsvp-font-family, sans-serif);
291
+ cursor: pointer;
292
+ transition: all var(--tsvp-transition-normal, 200ms) var(--tsvp-transition-easing, ease);
293
+ `
294
+
295
+ this.icon.style.cssText = `
296
+ display: flex;
297
+ align-items: center;
298
+ `
299
+ }
300
+
301
+ getElement(): HTMLButtonElement {
302
+ return this.button
303
+ }
304
+
305
+ show(): void {
306
+ this.visible = true
307
+ this.button.style.display = 'inline-flex'
308
+ }
309
+
310
+ hide(): void {
311
+ this.visible = false
312
+ this.button.style.display = 'none'
313
+ }
314
+
315
+ isVisible(): boolean {
316
+ return this.visible
317
+ }
318
+
319
+ onClick(handler: () => void): void {
320
+ this.button.addEventListener('click', handler)
321
+ }
322
+
323
+ destroy(): void {
324
+ this.button.remove()
325
+ }
326
+ }
327
+
328
+ // =============================================================================
329
+ // Live Controller
330
+ // =============================================================================
331
+
332
+ export class LiveController {
333
+ private indicator: LiveIndicator
334
+ private seekToLiveButton: SeekToLiveButton
335
+ private store: StateStore
336
+ private config: Required<LiveConfig>
337
+ private unsubscribe: (() => void) | null = null
338
+ private onSeekToLive: (() => void) | null = null
339
+
340
+ constructor(store: StateStore, config: LiveConfig = {}) {
341
+ this.store = store
342
+ this.config = {
343
+ liveEdgeThreshold: config.liveEdgeThreshold ?? 10,
344
+ autoSyncToLive: config.autoSyncToLive ?? true,
345
+ showLiveIndicator: config.showLiveIndicator ?? true,
346
+ showTimeBehind: config.showTimeBehind ?? true,
347
+ }
348
+
349
+ this.indicator = new LiveIndicator(this.config)
350
+ this.seekToLiveButton = new SeekToLiveButton()
351
+
352
+ // Subscribe to state changes
353
+ this.unsubscribe = this.store.subscribe('*', () => {
354
+ this.updateFromState()
355
+ })
356
+
357
+ // Handle indicator click
358
+ this.indicator.onClick(() => {
359
+ this.seekToLive()
360
+ })
361
+
362
+ // Handle button click
363
+ this.seekToLiveButton.onClick(() => {
364
+ this.seekToLive()
365
+ })
366
+ }
367
+
368
+ /**
369
+ * Update from player state
370
+ */
371
+ private updateFromState(): void {
372
+ const state = this.store.getState()
373
+
374
+ // Calculate live state
375
+ const isLive = state.streamType === 'live' || state.streamType === 'live:dvr'
376
+ const isDVR = state.streamType === 'live:dvr'
377
+
378
+ // Calculate seconds behind live
379
+ const seekableEnd = state.buffered.length > 0
380
+ ? state.buffered[state.buffered.length - 1].end
381
+ : state.duration
382
+ const secondsBehindLive = Math.max(0, seekableEnd - state.currentTime)
383
+ const isAtLiveEdge = secondsBehindLive <= this.config.liveEdgeThreshold
384
+
385
+ this.indicator.update({
386
+ isLive,
387
+ isAtLiveEdge,
388
+ secondsBehindLive,
389
+ isDVR,
390
+ liveEdgeTime: seekableEnd,
391
+ })
392
+
393
+ // Show/hide seek to live button
394
+ if (isLive && isDVR && !isAtLiveEdge) {
395
+ this.seekToLiveButton.show()
396
+ }
397
+ else {
398
+ this.seekToLiveButton.hide()
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Seek to live edge
404
+ */
405
+ seekToLive(): void {
406
+ if (this.onSeekToLive) {
407
+ this.onSeekToLive()
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Set seek to live callback
413
+ */
414
+ setSeekToLiveHandler(handler: () => void): void {
415
+ this.onSeekToLive = handler
416
+ }
417
+
418
+ /**
419
+ * Get live indicator element
420
+ */
421
+ getIndicator(): HTMLElement {
422
+ return this.indicator.getElement()
423
+ }
424
+
425
+ /**
426
+ * Get seek to live button element
427
+ */
428
+ getSeekToLiveButton(): HTMLElement {
429
+ return this.seekToLiveButton.getElement()
430
+ }
431
+
432
+ /**
433
+ * Get current live state
434
+ */
435
+ getState(): LiveState {
436
+ return this.indicator.getState()
437
+ }
438
+
439
+ /**
440
+ * Check if at live edge
441
+ */
442
+ isAtLiveEdge(): boolean {
443
+ return this.indicator.getState().isAtLiveEdge
444
+ }
445
+
446
+ /**
447
+ * Destroy controller
448
+ */
449
+ destroy(): void {
450
+ if (this.unsubscribe) {
451
+ this.unsubscribe()
452
+ }
453
+ this.indicator.destroy()
454
+ this.seekToLiveButton.destroy()
455
+ }
456
+ }
457
+
458
+ // =============================================================================
459
+ // Factory
460
+ // =============================================================================
461
+
462
+ export function createLiveIndicator(config?: LiveConfig): LiveIndicator {
463
+ return new LiveIndicator(config)
464
+ }
465
+
466
+ export function createSeekToLiveButton(): SeekToLiveButton {
467
+ return new SeekToLiveButton()
468
+ }
469
+
470
+ export function createLiveController(store: StateStore, config?: LiveConfig): LiveController {
471
+ return new LiveController(store, config)
472
+ }