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,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Text Tracks
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive text track management for captions and subtitles.
|
|
5
|
+
* Supports VTT, SRT, and external track loading.
|
|
6
|
+
*
|
|
7
|
+
* @module core/text-tracks
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TextTrack, TextTrackCue } from '../types'
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Text track ready state
|
|
18
|
+
*/
|
|
19
|
+
export enum TextTrackReadyState {
|
|
20
|
+
/** Not loaded */
|
|
21
|
+
NotLoaded = 0,
|
|
22
|
+
/** Loading */
|
|
23
|
+
Loading = 1,
|
|
24
|
+
/** Ready */
|
|
25
|
+
Ready = 2,
|
|
26
|
+
/** Error */
|
|
27
|
+
Error = 3,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Text track init options
|
|
32
|
+
*/
|
|
33
|
+
export interface TextTrackInit {
|
|
34
|
+
/** Track kind */
|
|
35
|
+
kind: TextTrack['kind']
|
|
36
|
+
/** Track label */
|
|
37
|
+
label: string
|
|
38
|
+
/** Track language (BCP 47) */
|
|
39
|
+
language: string
|
|
40
|
+
/** Track source URL */
|
|
41
|
+
src?: string
|
|
42
|
+
/** Whether this is the default track */
|
|
43
|
+
default?: boolean
|
|
44
|
+
/** Track content (VTT/SRT string) */
|
|
45
|
+
content?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* VTT cue init options
|
|
50
|
+
*/
|
|
51
|
+
export interface VTTCueInit {
|
|
52
|
+
/** Cue ID */
|
|
53
|
+
id?: string
|
|
54
|
+
/** Start time in seconds */
|
|
55
|
+
startTime: number
|
|
56
|
+
/** End time in seconds */
|
|
57
|
+
endTime: number
|
|
58
|
+
/** Cue text */
|
|
59
|
+
text: string
|
|
60
|
+
/** Vertical setting */
|
|
61
|
+
vertical?: '' | 'rl' | 'lr'
|
|
62
|
+
/** Line position */
|
|
63
|
+
line?: number | 'auto'
|
|
64
|
+
/** Line alignment */
|
|
65
|
+
lineAlign?: 'start' | 'center' | 'end'
|
|
66
|
+
/** Position */
|
|
67
|
+
position?: number | 'auto'
|
|
68
|
+
/** Position alignment */
|
|
69
|
+
positionAlign?: 'line-left' | 'center' | 'line-right' | 'auto'
|
|
70
|
+
/** Size (percentage) */
|
|
71
|
+
size?: number
|
|
72
|
+
/** Alignment */
|
|
73
|
+
align?: 'start' | 'center' | 'end' | 'left' | 'right'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Text Track List
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Manages a list of text tracks
|
|
82
|
+
*/
|
|
83
|
+
export class TextTrackList {
|
|
84
|
+
private tracks: ManagedTextTrack[] = []
|
|
85
|
+
private listeners = new Set<(tracks: TextTrack[]) => void>()
|
|
86
|
+
private video: HTMLVideoElement | null = null
|
|
87
|
+
|
|
88
|
+
constructor(video?: HTMLVideoElement) {
|
|
89
|
+
this.video = video || null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all tracks
|
|
94
|
+
*/
|
|
95
|
+
getAll(): TextTrack[] {
|
|
96
|
+
return this.tracks.map((t) => t.toJSON())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get track by ID
|
|
101
|
+
*/
|
|
102
|
+
get(id: string): TextTrack | null {
|
|
103
|
+
const track = this.tracks.find((t) => t.id === id)
|
|
104
|
+
return track?.toJSON() || null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get currently showing track
|
|
109
|
+
*/
|
|
110
|
+
getShowing(): TextTrack | null {
|
|
111
|
+
const track = this.tracks.find((t) => t.mode === 'showing')
|
|
112
|
+
return track?.toJSON() || null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Add a new text track
|
|
117
|
+
*/
|
|
118
|
+
async add(init: TextTrackInit): Promise<TextTrack> {
|
|
119
|
+
const id = `track-${this.tracks.length}`
|
|
120
|
+
const track = new ManagedTextTrack({
|
|
121
|
+
id,
|
|
122
|
+
...init,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
this.tracks.push(track)
|
|
126
|
+
|
|
127
|
+
// Load track if src provided
|
|
128
|
+
if (init.src) {
|
|
129
|
+
await track.load(init.src)
|
|
130
|
+
} else if (init.content) {
|
|
131
|
+
track.parseContent(init.content)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add to video element if available
|
|
135
|
+
if (this.video) {
|
|
136
|
+
this.addToVideo(track)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.notify()
|
|
140
|
+
return track.toJSON()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Remove a text track
|
|
145
|
+
*/
|
|
146
|
+
remove(id: string): void {
|
|
147
|
+
const index = this.tracks.findIndex((t) => t.id === id)
|
|
148
|
+
if (index !== -1) {
|
|
149
|
+
const track = this.tracks[index]
|
|
150
|
+
track.dispose()
|
|
151
|
+
this.tracks.splice(index, 1)
|
|
152
|
+
this.notify()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set track mode
|
|
158
|
+
*/
|
|
159
|
+
setMode(id: string, mode: TextTrack['mode']): void {
|
|
160
|
+
const track = this.tracks.find((t) => t.id === id)
|
|
161
|
+
if (!track) return
|
|
162
|
+
|
|
163
|
+
// Disable other showing tracks if enabling this one
|
|
164
|
+
if (mode === 'showing') {
|
|
165
|
+
for (const t of this.tracks) {
|
|
166
|
+
if (t.id !== id && t.mode === 'showing') {
|
|
167
|
+
t.mode = 'disabled'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
track.mode = mode
|
|
173
|
+
this.notify()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get active cues at a specific time
|
|
178
|
+
*/
|
|
179
|
+
getActiveCues(time: number): TextTrackCue[] {
|
|
180
|
+
const showing = this.tracks.find((t) => t.mode === 'showing')
|
|
181
|
+
if (!showing) return []
|
|
182
|
+
|
|
183
|
+
return showing.cues.filter((cue) => time >= cue.startTime && time <= cue.endTime)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Subscribe to track changes
|
|
188
|
+
*/
|
|
189
|
+
subscribe(listener: (tracks: TextTrack[]) => void): () => void {
|
|
190
|
+
this.listeners.add(listener)
|
|
191
|
+
return () => this.listeners.delete(listener)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear all tracks
|
|
196
|
+
*/
|
|
197
|
+
clear(): void {
|
|
198
|
+
for (const track of this.tracks) {
|
|
199
|
+
track.dispose()
|
|
200
|
+
}
|
|
201
|
+
this.tracks = []
|
|
202
|
+
this.notify()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private addToVideo(track: ManagedTextTrack): void {
|
|
206
|
+
if (!this.video) return
|
|
207
|
+
|
|
208
|
+
// Create native track element
|
|
209
|
+
const trackEl = document.createElement('track')
|
|
210
|
+
trackEl.kind = track.kind
|
|
211
|
+
trackEl.label = track.label
|
|
212
|
+
trackEl.srclang = track.language
|
|
213
|
+
if (track.src) trackEl.src = track.src
|
|
214
|
+
if (track.default) trackEl.default = true
|
|
215
|
+
|
|
216
|
+
this.video.appendChild(trackEl)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private notify(): void {
|
|
220
|
+
const tracks = this.getAll()
|
|
221
|
+
this.listeners.forEach((l) => l(tracks))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// Managed Text Track
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
class ManagedTextTrack {
|
|
230
|
+
readonly id: string
|
|
231
|
+
readonly kind: TextTrack['kind']
|
|
232
|
+
readonly label: string
|
|
233
|
+
readonly language: string
|
|
234
|
+
readonly src?: string
|
|
235
|
+
readonly default: boolean
|
|
236
|
+
|
|
237
|
+
private _mode: TextTrack['mode'] = 'disabled'
|
|
238
|
+
private _cues: TextTrackCue[] = []
|
|
239
|
+
private _readyState: TextTrackReadyState = TextTrackReadyState.NotLoaded
|
|
240
|
+
|
|
241
|
+
constructor(init: TextTrackInit & { id: string }) {
|
|
242
|
+
this.id = init.id
|
|
243
|
+
this.kind = init.kind
|
|
244
|
+
this.label = init.label
|
|
245
|
+
this.language = init.language
|
|
246
|
+
this.src = init.src
|
|
247
|
+
this.default = init.default || false
|
|
248
|
+
|
|
249
|
+
if (init.default) {
|
|
250
|
+
this._mode = 'showing'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get mode(): TextTrack['mode'] {
|
|
255
|
+
return this._mode
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
set mode(value: TextTrack['mode']) {
|
|
259
|
+
this._mode = value
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get cues(): TextTrackCue[] {
|
|
263
|
+
return this._cues
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
get readyState(): TextTrackReadyState {
|
|
267
|
+
return this._readyState
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Load track from URL
|
|
272
|
+
*/
|
|
273
|
+
async load(url: string): Promise<void> {
|
|
274
|
+
this._readyState = TextTrackReadyState.Loading
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(url)
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
throw new Error(`Failed to load track: ${response.status}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const content = await response.text()
|
|
283
|
+
this.parseContent(content)
|
|
284
|
+
this._readyState = TextTrackReadyState.Ready
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this._readyState = TextTrackReadyState.Error
|
|
287
|
+
console.warn('[ts-video-player] Failed to load text track:', error)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse track content (VTT or SRT)
|
|
293
|
+
*/
|
|
294
|
+
parseContent(content: string): void {
|
|
295
|
+
// Detect format
|
|
296
|
+
if (content.trim().startsWith('WEBVTT')) {
|
|
297
|
+
this._cues = parseVTT(content)
|
|
298
|
+
} else {
|
|
299
|
+
this._cues = parseSRT(content)
|
|
300
|
+
}
|
|
301
|
+
this._readyState = TextTrackReadyState.Ready
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Add a cue
|
|
306
|
+
*/
|
|
307
|
+
addCue(cue: VTTCueInit): void {
|
|
308
|
+
this._cues.push({
|
|
309
|
+
id: cue.id || `cue-${this._cues.length}`,
|
|
310
|
+
startTime: cue.startTime,
|
|
311
|
+
endTime: cue.endTime,
|
|
312
|
+
text: cue.text,
|
|
313
|
+
position: cue.position,
|
|
314
|
+
line: cue.line,
|
|
315
|
+
align: cue.align,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Remove a cue
|
|
321
|
+
*/
|
|
322
|
+
removeCue(id: string): void {
|
|
323
|
+
const index = this._cues.findIndex((c) => c.id === id)
|
|
324
|
+
if (index !== -1) {
|
|
325
|
+
this._cues.splice(index, 1)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Convert to plain object
|
|
331
|
+
*/
|
|
332
|
+
toJSON(): TextTrack {
|
|
333
|
+
return {
|
|
334
|
+
id: this.id,
|
|
335
|
+
kind: this.kind,
|
|
336
|
+
label: this.label,
|
|
337
|
+
language: this.language,
|
|
338
|
+
src: this.src,
|
|
339
|
+
default: this.default,
|
|
340
|
+
mode: this._mode,
|
|
341
|
+
cues: this._cues,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Dispose the track
|
|
347
|
+
*/
|
|
348
|
+
dispose(): void {
|
|
349
|
+
this._cues = []
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// VTT Parser
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Parse VTT content
|
|
359
|
+
*/
|
|
360
|
+
function parseVTT(content: string): TextTrackCue[] {
|
|
361
|
+
const cues: TextTrackCue[] = []
|
|
362
|
+
const lines = content.split(/\r?\n/)
|
|
363
|
+
|
|
364
|
+
let i = 0
|
|
365
|
+
|
|
366
|
+
// Skip header
|
|
367
|
+
while (i < lines.length && !lines[i].includes('-->')) {
|
|
368
|
+
i++
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
while (i < lines.length) {
|
|
372
|
+
const line = lines[i].trim()
|
|
373
|
+
|
|
374
|
+
// Check for cue timing line
|
|
375
|
+
if (line.includes('-->')) {
|
|
376
|
+
const [start, end] = line.split('-->').map((t) => parseVTTTime(t.trim()))
|
|
377
|
+
|
|
378
|
+
// Collect cue text
|
|
379
|
+
i++
|
|
380
|
+
const textLines: string[] = []
|
|
381
|
+
while (i < lines.length && lines[i].trim() !== '') {
|
|
382
|
+
textLines.push(lines[i])
|
|
383
|
+
i++
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
cues.push({
|
|
387
|
+
id: `cue-${cues.length}`,
|
|
388
|
+
startTime: start,
|
|
389
|
+
endTime: end,
|
|
390
|
+
text: textLines.join('\n'),
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
i++
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return cues
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Parse VTT time format (00:00:00.000 or 00:00.000)
|
|
402
|
+
*/
|
|
403
|
+
function parseVTTTime(time: string): number {
|
|
404
|
+
const parts = time.split(':')
|
|
405
|
+
let seconds = 0
|
|
406
|
+
|
|
407
|
+
if (parts.length === 3) {
|
|
408
|
+
// HH:MM:SS.mmm
|
|
409
|
+
seconds = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseFloat(parts[2])
|
|
410
|
+
} else if (parts.length === 2) {
|
|
411
|
+
// MM:SS.mmm
|
|
412
|
+
seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1])
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return seconds
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// =============================================================================
|
|
419
|
+
// SRT Parser
|
|
420
|
+
// =============================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse SRT content
|
|
424
|
+
*/
|
|
425
|
+
function parseSRT(content: string): TextTrackCue[] {
|
|
426
|
+
const cues: TextTrackCue[] = []
|
|
427
|
+
const blocks = content.trim().split(/\r?\n\r?\n/)
|
|
428
|
+
|
|
429
|
+
for (const block of blocks) {
|
|
430
|
+
const lines = block.split(/\r?\n/)
|
|
431
|
+
if (lines.length < 2) continue
|
|
432
|
+
|
|
433
|
+
// Find timing line
|
|
434
|
+
let timingIndex = 0
|
|
435
|
+
while (timingIndex < lines.length && !lines[timingIndex].includes('-->')) {
|
|
436
|
+
timingIndex++
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (timingIndex >= lines.length) continue
|
|
440
|
+
|
|
441
|
+
const timingLine = lines[timingIndex]
|
|
442
|
+
const [start, end] = timingLine.split('-->').map((t) => parseSRTTime(t.trim()))
|
|
443
|
+
|
|
444
|
+
// Collect text
|
|
445
|
+
const textLines = lines.slice(timingIndex + 1)
|
|
446
|
+
|
|
447
|
+
cues.push({
|
|
448
|
+
id: lines[0] || `cue-${cues.length}`,
|
|
449
|
+
startTime: start,
|
|
450
|
+
endTime: end,
|
|
451
|
+
text: textLines.join('\n'),
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return cues
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Parse SRT time format (00:00:00,000)
|
|
460
|
+
*/
|
|
461
|
+
function parseSRTTime(time: string): number {
|
|
462
|
+
// SRT uses comma for milliseconds
|
|
463
|
+
const normalized = time.replace(',', '.')
|
|
464
|
+
return parseVTTTime(normalized)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// =============================================================================
|
|
468
|
+
// Caption Renderer
|
|
469
|
+
// =============================================================================
|
|
470
|
+
|
|
471
|
+
export interface CaptionRendererOptions {
|
|
472
|
+
/** Container element */
|
|
473
|
+
container: HTMLElement
|
|
474
|
+
/** Font size */
|
|
475
|
+
fontSize?: number
|
|
476
|
+
/** Font family */
|
|
477
|
+
fontFamily?: string
|
|
478
|
+
/** Text color */
|
|
479
|
+
color?: string
|
|
480
|
+
/** Background color */
|
|
481
|
+
backgroundColor?: string
|
|
482
|
+
/** Background opacity */
|
|
483
|
+
backgroundOpacity?: number
|
|
484
|
+
/** Text shadow */
|
|
485
|
+
textShadow?: boolean
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Render captions on screen
|
|
490
|
+
*/
|
|
491
|
+
export class CaptionRenderer {
|
|
492
|
+
private container: HTMLElement
|
|
493
|
+
private element: HTMLElement | null = null
|
|
494
|
+
private options: Required<Omit<CaptionRendererOptions, 'container'>>
|
|
495
|
+
|
|
496
|
+
constructor(options: CaptionRendererOptions) {
|
|
497
|
+
this.container = options.container
|
|
498
|
+
this.options = {
|
|
499
|
+
fontSize: options.fontSize || 18,
|
|
500
|
+
fontFamily: options.fontFamily || 'sans-serif',
|
|
501
|
+
color: options.color || '#ffffff',
|
|
502
|
+
backgroundColor: options.backgroundColor || '#000000',
|
|
503
|
+
backgroundOpacity: options.backgroundOpacity || 0.75,
|
|
504
|
+
textShadow: options.textShadow !== false,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.createCaptionElement()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private createCaptionElement(): void {
|
|
511
|
+
this.element = document.createElement('div')
|
|
512
|
+
this.element.className = 'ts-video-player__captions'
|
|
513
|
+
this.element.setAttribute('aria-live', 'polite')
|
|
514
|
+
this.element.setAttribute('aria-atomic', 'true')
|
|
515
|
+
this.applyStyles()
|
|
516
|
+
this.container.appendChild(this.element)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private applyStyles(): void {
|
|
520
|
+
if (!this.element) return
|
|
521
|
+
|
|
522
|
+
const bgColor = this.hexToRgba(this.options.backgroundColor, this.options.backgroundOpacity)
|
|
523
|
+
|
|
524
|
+
Object.assign(this.element.style, {
|
|
525
|
+
position: 'absolute',
|
|
526
|
+
bottom: '10%',
|
|
527
|
+
left: '50%',
|
|
528
|
+
transform: 'translateX(-50%)',
|
|
529
|
+
maxWidth: '80%',
|
|
530
|
+
textAlign: 'center',
|
|
531
|
+
fontSize: `${this.options.fontSize}px`,
|
|
532
|
+
fontFamily: this.options.fontFamily,
|
|
533
|
+
color: this.options.color,
|
|
534
|
+
backgroundColor: bgColor,
|
|
535
|
+
padding: '4px 8px',
|
|
536
|
+
borderRadius: '4px',
|
|
537
|
+
lineHeight: '1.4',
|
|
538
|
+
pointerEvents: 'none',
|
|
539
|
+
zIndex: '10',
|
|
540
|
+
whiteSpace: 'pre-wrap',
|
|
541
|
+
textShadow: this.options.textShadow ? '0 1px 2px rgba(0,0,0,0.5)' : 'none',
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Update the displayed text
|
|
547
|
+
*/
|
|
548
|
+
setText(text: string): void {
|
|
549
|
+
if (!this.element) return
|
|
550
|
+
|
|
551
|
+
if (text) {
|
|
552
|
+
this.element.textContent = text
|
|
553
|
+
this.element.style.display = 'block'
|
|
554
|
+
} else {
|
|
555
|
+
this.element.style.display = 'none'
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Update renderer options
|
|
561
|
+
*/
|
|
562
|
+
setOptions(options: Partial<Omit<CaptionRendererOptions, 'container'>>): void {
|
|
563
|
+
Object.assign(this.options, options)
|
|
564
|
+
this.applyStyles()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Destroy the renderer
|
|
569
|
+
*/
|
|
570
|
+
destroy(): void {
|
|
571
|
+
this.element?.remove()
|
|
572
|
+
this.element = null
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private hexToRgba(hex: string, alpha: number): string {
|
|
576
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
577
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
578
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
579
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// =============================================================================
|
|
584
|
+
// Factory Functions
|
|
585
|
+
// =============================================================================
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Create a new text track list
|
|
589
|
+
*/
|
|
590
|
+
export function createTextTrackList(video?: HTMLVideoElement): TextTrackList {
|
|
591
|
+
return new TextTrackList(video)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Create a caption renderer
|
|
596
|
+
*/
|
|
597
|
+
export function createCaptionRenderer(options: CaptionRendererOptions): CaptionRenderer {
|
|
598
|
+
return new CaptionRenderer(options)
|
|
599
|
+
}
|