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,476 @@
1
+ /**
2
+ * ts-video-player State Management
3
+ *
4
+ * Simple reactive state system for the video player.
5
+ *
6
+ * @module core/state
7
+ */
8
+
9
+ import type { PlayerState } from '../types'
10
+
11
+ // =============================================================================
12
+ // Utilities
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Shallow equality comparison for plain objects.
17
+ * Returns true if both objects have the same keys with === equal values.
18
+ * Used to optimize selector subscriptions — skip notifications when
19
+ * derived state hasn't meaningfully changed.
20
+ */
21
+ export function shallowEqual<T extends Record<string, unknown>>(a: T, b: T): boolean {
22
+ if (a === b) return true
23
+ if (!a || !b) return false
24
+
25
+ const keysA = Object.keys(a)
26
+ const keysB = Object.keys(b)
27
+
28
+ if (keysA.length !== keysB.length) return false
29
+
30
+ for (const key of keysA) {
31
+ if (a[key] !== b[key]) return false
32
+ }
33
+
34
+ return true
35
+ }
36
+
37
+ // =============================================================================
38
+ // Reactive Signal System
39
+ // =============================================================================
40
+
41
+ // eslint-disable-next-line pickier/no-unused-vars
42
+ type Listener<T> = (_value: T, _prev: T) => void
43
+ type Unsubscribe = () => void
44
+
45
+ /**
46
+ * Create a reactive signal
47
+ */
48
+ export function createSignal<T>(initial: T): [() => T, (value: T | ((prev: T) => T)) => void] {
49
+ let value = initial
50
+ const listeners = new Set<Listener<T>>()
51
+
52
+ const get = () => value
53
+
54
+ const set = (newValue: T | ((prev: T) => T)) => {
55
+ const prev = value
56
+ value = typeof newValue === 'function' ? (newValue as (prev: T) => T)(prev) : newValue
57
+ if (value !== prev) {
58
+ listeners.forEach((listener) => listener(value, prev))
59
+ }
60
+ }
61
+
62
+ // Attach subscribe method to getter
63
+ ;(get as any).subscribe = (listener: Listener<T>): Unsubscribe => {
64
+ listeners.add(listener)
65
+ return () => listeners.delete(listener)
66
+ }
67
+
68
+ return [get, set]
69
+ }
70
+
71
+ /**
72
+ * Create a computed value from signals
73
+ */
74
+ export function computed<T>(fn: () => T, deps: Array<() => unknown>): () => T {
75
+ let value: T
76
+ let dirty = true
77
+
78
+ // Subscribe to all dependencies
79
+ deps.forEach((dep) => {
80
+ if ((dep as any).subscribe) {
81
+ (dep as any).subscribe(() => {
82
+ dirty = true
83
+ })
84
+ }
85
+ })
86
+
87
+ return () => {
88
+ if (dirty) {
89
+ value = fn()
90
+ dirty = false
91
+ }
92
+ return value
93
+ }
94
+ }
95
+
96
+ // =============================================================================
97
+ // Player State Store
98
+ // =============================================================================
99
+
100
+ /**
101
+ * Default player state
102
+ */
103
+ export function createDefaultState(): PlayerState {
104
+ return {
105
+ // Source
106
+ src: null,
107
+ sources: [],
108
+ currentSourceIndex: 0,
109
+ mediaType: 'unknown',
110
+ streamType: 'unknown',
111
+ providerType: 'unknown',
112
+
113
+ // Loading
114
+ loadingState: 'idle',
115
+ preload: 'metadata',
116
+ canPlay: false,
117
+ canPlayThrough: false,
118
+ error: null,
119
+
120
+ // Playback
121
+ playbackState: 'idle',
122
+ paused: true,
123
+ playing: false,
124
+ started: false,
125
+ ended: false,
126
+ seeking: false,
127
+ waiting: false,
128
+ currentTime: 0,
129
+ duration: 0,
130
+ playbackRate: 1,
131
+ loop: false,
132
+ autoplay: false,
133
+ playsinline: true,
134
+
135
+ // Volume
136
+ muted: false,
137
+ volume: 1,
138
+
139
+ // Buffering
140
+ buffered: [],
141
+ bufferedAmount: 0,
142
+ seekable: [],
143
+
144
+ // Dimensions
145
+ videoWidth: 0,
146
+ videoHeight: 0,
147
+ aspectRatio: 16 / 9,
148
+
149
+ // Tracks
150
+ qualities: [],
151
+ autoQuality: true,
152
+ audioTracks: [],
153
+ textTracks: [],
154
+
155
+ // Fullscreen/PiP
156
+ fullscreen: false,
157
+ pictureInPicture: false,
158
+ canFullscreen: false,
159
+ canPictureInPicture: false,
160
+
161
+ // Feature Availability
162
+ volumeAvailability: 'unavailable',
163
+ fullscreenAvailability: 'unavailable',
164
+ pipAvailability: 'unavailable',
165
+
166
+ // UI
167
+ controlsVisible: true,
168
+ userActive: true,
169
+ pointerOver: false,
170
+
171
+ // Metadata
172
+ title: '',
173
+ poster: '',
174
+ }
175
+ }
176
+
177
+ /**
178
+ * State store for the player
179
+ *
180
+ * Uses microtask-coalesced notifications: multiple rapid set() calls within
181
+ * the same microtask are batched into a single notification cycle. This prevents
182
+ * redundant re-renders when many state fields change in quick succession
183
+ * (e.g., timeupdate + progress + buffered all firing together).
184
+ */
185
+ export class StateStore {
186
+ private state: PlayerState
187
+ private listeners = new Map<keyof PlayerState | '*', Set<(state: PlayerState, key?: keyof PlayerState) => void>>()
188
+ private _pendingKeys: Set<keyof PlayerState> | null = null
189
+ private _flushScheduled = false
190
+
191
+ constructor(initial?: Partial<PlayerState>) {
192
+ this.state = { ...createDefaultState(), ...initial }
193
+ }
194
+
195
+ /**
196
+ * Get the current state
197
+ */
198
+ getState(): Readonly<PlayerState> {
199
+ return this.state
200
+ }
201
+
202
+ /**
203
+ * Get a specific state value
204
+ */
205
+ get<K extends keyof PlayerState>(key: K): PlayerState[K] {
206
+ return this.state[key]
207
+ }
208
+
209
+ /**
210
+ * Set a single state value. Notifications are coalesced via microtask.
211
+ */
212
+ set<K extends keyof PlayerState>(key: K, value: PlayerState[K]): void {
213
+ if (this.state[key] === value) return
214
+
215
+ this.state = { ...this.state, [key]: value }
216
+ this.scheduleNotify(key)
217
+ }
218
+
219
+ /**
220
+ * Batch update multiple state values. Notifications are coalesced via microtask.
221
+ */
222
+ batch(updates: Partial<PlayerState>): void {
223
+ for (const [key, value] of Object.entries(updates)) {
224
+ if (this.state[key as keyof PlayerState] !== value) {
225
+ ;(this.state as any)[key] = value
226
+ this.scheduleNotify(key as keyof PlayerState)
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Subscribe to state changes
233
+ */
234
+ subscribe(
235
+ // eslint-disable-next-line pickier/no-unused-vars
236
+ keyOrListener: keyof PlayerState | '*' | ((_state: PlayerState, _key?: keyof PlayerState) => void),
237
+ // eslint-disable-next-line pickier/no-unused-vars
238
+ listener?: (_state: PlayerState, _key?: keyof PlayerState) => void,
239
+ ): Unsubscribe {
240
+ if (typeof keyOrListener === 'function') {
241
+ // Subscribe to all changes
242
+ const key = '*'
243
+ if (!this.listeners.has(key)) {
244
+ this.listeners.set(key, new Set())
245
+ }
246
+ this.listeners.get(key)!.add(keyOrListener)
247
+ return () => this.listeners.get(key)?.delete(keyOrListener)
248
+ }
249
+
250
+ // Subscribe to specific key
251
+ const key = keyOrListener
252
+ if (!listener) return () => {}
253
+
254
+ if (!this.listeners.has(key)) {
255
+ this.listeners.set(key, new Set())
256
+ }
257
+ this.listeners.get(key)!.add(listener)
258
+ return () => this.listeners.get(key)?.delete(listener)
259
+ }
260
+
261
+ /**
262
+ * Reset state to defaults
263
+ */
264
+ reset(): void {
265
+ this.state = createDefaultState()
266
+ this._pendingKeys = null
267
+ this._flushScheduled = false
268
+ this.notifyAll()
269
+ }
270
+
271
+ /**
272
+ * Force synchronous flush of pending notifications.
273
+ * Useful in tests or when immediate consistency is required.
274
+ */
275
+ flush(): void {
276
+ if (this._pendingKeys) {
277
+ const keys = this._pendingKeys
278
+ this._pendingKeys = null
279
+ this._flushScheduled = false
280
+ for (const key of keys) {
281
+ this.notifyKey(key)
282
+ }
283
+ this.notifyAll()
284
+ }
285
+ }
286
+
287
+ private scheduleNotify(key: keyof PlayerState): void {
288
+ if (!this._pendingKeys) {
289
+ this._pendingKeys = new Set()
290
+ }
291
+ this._pendingKeys.add(key)
292
+
293
+ if (!this._flushScheduled) {
294
+ this._flushScheduled = true
295
+ queueMicrotask(() => this.flush())
296
+ }
297
+ }
298
+
299
+ private notifyKey(key: keyof PlayerState): void {
300
+ this.listeners.get(key)?.forEach((listener) => listener(this.state, key))
301
+ }
302
+
303
+ private notifyAll(): void {
304
+ this.listeners.get('*')?.forEach((listener) => listener(this.state))
305
+ }
306
+ }
307
+
308
+ // =============================================================================
309
+ // State Selectors (Computed Values)
310
+ // =============================================================================
311
+
312
+ /**
313
+ * Calculate buffered percentage
314
+ */
315
+ export function selectBufferedAmount(state: PlayerState): number {
316
+ if (state.duration === 0 || state.buffered.length === 0) return 0
317
+
318
+ let buffered = 0
319
+ for (const range of state.buffered) {
320
+ buffered += range.end - range.start
321
+ }
322
+ return Math.min(buffered / state.duration, 1)
323
+ }
324
+
325
+ /**
326
+ * Calculate progress percentage
327
+ */
328
+ export function selectProgress(state: PlayerState): number {
329
+ if (state.duration === 0) return 0
330
+ return Math.min(state.currentTime / state.duration, 1)
331
+ }
332
+
333
+ /**
334
+ * Get remaining time
335
+ */
336
+ export function selectRemainingTime(state: PlayerState): number {
337
+ return Math.max(state.duration - state.currentTime, 0)
338
+ }
339
+
340
+ /**
341
+ * Check if player is in idle state
342
+ */
343
+ export function selectIsIdle(state: PlayerState): boolean {
344
+ return state.loadingState === 'idle' && state.playbackState === 'idle'
345
+ }
346
+
347
+ /**
348
+ * Check if player is loading
349
+ */
350
+ export function selectIsLoading(state: PlayerState): boolean {
351
+ return state.loadingState === 'loading' || state.waiting
352
+ }
353
+
354
+ /**
355
+ * Check if live stream
356
+ */
357
+ export function selectIsLive(state: PlayerState): boolean {
358
+ return state.streamType.includes('live')
359
+ }
360
+
361
+ /**
362
+ * Check if DVR enabled
363
+ */
364
+ export function selectIsDVR(state: PlayerState): boolean {
365
+ return state.streamType === 'live:dvr'
366
+ }
367
+
368
+ /**
369
+ * Get current quality
370
+ */
371
+ export function selectCurrentQuality(state: PlayerState): string | null {
372
+ const selected = state.qualities.find((q) => q.selected)
373
+ if (!selected) return state.autoQuality ? 'auto' : null
374
+ return `${selected.height}p`
375
+ }
376
+
377
+ /**
378
+ * Get current text track
379
+ */
380
+ export function selectCurrentTextTrack(state: PlayerState): string | null {
381
+ const showing = state.textTracks.find((t) => t.mode === 'showing')
382
+ return showing?.label || null
383
+ }
384
+
385
+ /**
386
+ * Get current audio track
387
+ */
388
+ export function selectCurrentAudioTrack(state: PlayerState): string | null {
389
+ const selected = state.audioTracks.find((t) => t.selected)
390
+ return selected?.label || null
391
+ }
392
+
393
+ // =============================================================================
394
+ // Deprecated Selectors (use availability states instead)
395
+ // =============================================================================
396
+
397
+ /** @deprecated Use `state.fullscreenAvailability === 'available'` instead */
398
+ export function selectCanFullscreen(state: PlayerState): boolean {
399
+ return state.fullscreenAvailability === 'available'
400
+ }
401
+
402
+ /** @deprecated Use `state.pipAvailability === 'available'` instead */
403
+ export function selectCanPiP(state: PlayerState): boolean {
404
+ return state.pipAvailability === 'available'
405
+ }
406
+
407
+ /** @deprecated Use `state.volumeAvailability === 'available'` instead */
408
+ export function selectCanSetVolume(state: PlayerState): boolean {
409
+ return state.volumeAvailability === 'available'
410
+ }
411
+
412
+ // =============================================================================
413
+ // Storage Integration
414
+ // =============================================================================
415
+
416
+ const STORAGE_KEY = 'ts-video-player'
417
+
418
+ interface StoredState {
419
+ volume: number
420
+ muted: boolean
421
+ playbackRate: number
422
+ captions: boolean
423
+ quality: string
424
+ }
425
+
426
+ /**
427
+ * Load persisted state from storage
428
+ */
429
+ export function loadPersistedState(storage: Storage = localStorage): Partial<StoredState> {
430
+ try {
431
+ const data = storage.getItem(STORAGE_KEY)
432
+ if (data) {
433
+ return JSON.parse(data)
434
+ }
435
+ } catch {
436
+ // Ignore storage errors
437
+ }
438
+ return {}
439
+ }
440
+
441
+ /**
442
+ * Save state to storage
443
+ */
444
+ export function savePersistedState(state: Partial<StoredState>, storage: Storage = localStorage): void {
445
+ try {
446
+ const existing = loadPersistedState(storage)
447
+ storage.setItem(STORAGE_KEY, JSON.stringify({ ...existing, ...state }))
448
+ } catch {
449
+ // Ignore storage errors
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Create storage sync for state store
455
+ */
456
+ export function createStorageSync(
457
+ store: StateStore,
458
+ keys: (keyof StoredState)[] = ['volume', 'muted', 'playbackRate'],
459
+ storage: Storage = localStorage,
460
+ ): Unsubscribe {
461
+ // Load initial values
462
+ const persisted = loadPersistedState(storage)
463
+ if (persisted.volume !== undefined) store.set('volume', persisted.volume)
464
+ if (persisted.muted !== undefined) store.set('muted', persisted.muted)
465
+ if (persisted.playbackRate !== undefined) store.set('playbackRate', persisted.playbackRate)
466
+
467
+ // Subscribe to changes
468
+ const unsubscribes = keys.map((key) =>
469
+ store.subscribe(key as keyof PlayerState, (state) => {
470
+ const value = state[key as keyof PlayerState]
471
+ savePersistedState({ [key]: value } as Partial<StoredState>, storage)
472
+ }),
473
+ )
474
+
475
+ return () => unsubscribes.forEach((unsub) => unsub())
476
+ }