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,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
|
+
}
|