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,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player HTML5 Provider
|
|
3
|
+
*
|
|
4
|
+
* Native HTML5 video/audio element provider.
|
|
5
|
+
*
|
|
6
|
+
* @module providers/html5
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ProviderType,
|
|
11
|
+
Src,
|
|
12
|
+
MediaSource,
|
|
13
|
+
PlayerOptions,
|
|
14
|
+
AudioTrack,
|
|
15
|
+
TextTrack,
|
|
16
|
+
} from '../types'
|
|
17
|
+
import { BaseProvider } from './base'
|
|
18
|
+
import { MediaEventsNormalizer, onFullscreenChange, onPiPChange } from '../core/events'
|
|
19
|
+
import { probeVolumeAvailability } from '../core/features'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a source is an HTML5 compatible source
|
|
23
|
+
*/
|
|
24
|
+
export function isHTML5Source(src: Src): boolean {
|
|
25
|
+
if (typeof src === 'string') {
|
|
26
|
+
const extension = getExtension(src)
|
|
27
|
+
// Check for supported extensions
|
|
28
|
+
if (
|
|
29
|
+
extension === 'mp4' ||
|
|
30
|
+
extension === 'webm' ||
|
|
31
|
+
extension === 'ogg' ||
|
|
32
|
+
extension === 'ogv' ||
|
|
33
|
+
extension === 'mov' ||
|
|
34
|
+
extension === 'mp3' ||
|
|
35
|
+
extension === 'wav' ||
|
|
36
|
+
extension === 'flac' ||
|
|
37
|
+
extension === 'm4a' ||
|
|
38
|
+
extension === 'aac' ||
|
|
39
|
+
extension === 'oga' ||
|
|
40
|
+
extension === 'opus'
|
|
41
|
+
) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
// Check for MIME types
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof src === 'object' && 'type' in src) {
|
|
49
|
+
const type = (src as MediaSource).type?.toLowerCase() || ''
|
|
50
|
+
return (
|
|
51
|
+
type.startsWith('video/') ||
|
|
52
|
+
type.startsWith('audio/') ||
|
|
53
|
+
type === 'application/ogg'
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getExtension(src: string): string {
|
|
61
|
+
const path = src.split(/[?#]/, 1)[0]
|
|
62
|
+
return path.slice(path.lastIndexOf('.') + 1).toLowerCase()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get MIME type for a source
|
|
67
|
+
*/
|
|
68
|
+
function getMimeType(src: string): string {
|
|
69
|
+
const ext = getExtension(src)
|
|
70
|
+
const mimeTypes: Record<string, string> = {
|
|
71
|
+
mp4: 'video/mp4',
|
|
72
|
+
webm: 'video/webm',
|
|
73
|
+
ogg: 'video/ogg',
|
|
74
|
+
ogv: 'video/ogg',
|
|
75
|
+
mov: 'video/quicktime',
|
|
76
|
+
mp3: 'audio/mpeg',
|
|
77
|
+
wav: 'audio/wav',
|
|
78
|
+
flac: 'audio/flac',
|
|
79
|
+
m4a: 'audio/mp4',
|
|
80
|
+
aac: 'audio/aac',
|
|
81
|
+
oga: 'audio/ogg',
|
|
82
|
+
opus: 'audio/ogg; codecs=opus',
|
|
83
|
+
}
|
|
84
|
+
return mimeTypes[ext || ''] || 'video/mp4'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* HTML5 Video/Audio Provider
|
|
89
|
+
*/
|
|
90
|
+
export class HTML5Provider extends BaseProvider {
|
|
91
|
+
readonly name = 'html5'
|
|
92
|
+
readonly type: ProviderType = 'video'
|
|
93
|
+
|
|
94
|
+
private media: HTMLVideoElement | HTMLAudioElement | null = null
|
|
95
|
+
private eventsNormalizer: MediaEventsNormalizer | null = null
|
|
96
|
+
private cleanupFullscreen: (() => void) | null = null
|
|
97
|
+
private cleanupPiP: (() => void) | null = null
|
|
98
|
+
private isAudio = false
|
|
99
|
+
|
|
100
|
+
get mediaElement(): HTMLVideoElement | HTMLAudioElement | null {
|
|
101
|
+
return this.media
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// === Lifecycle ===
|
|
105
|
+
|
|
106
|
+
protected async initialize(): Promise<void> {
|
|
107
|
+
if (!this.container) {
|
|
108
|
+
throw new Error('Container element is required')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Determine if audio or video based on options
|
|
112
|
+
this.isAudio = this.options.mediaType === 'audio'
|
|
113
|
+
|
|
114
|
+
// Create media element
|
|
115
|
+
this.media = document.createElement(this.isAudio ? 'audio' : 'video')
|
|
116
|
+
this.media.className = 'ts-video-player__media'
|
|
117
|
+
|
|
118
|
+
// Set attributes
|
|
119
|
+
if (this.options.autoplay) this.media.autoplay = true
|
|
120
|
+
if (this.options.loop) this.media.loop = true
|
|
121
|
+
if (this.options.muted) this.media.muted = true
|
|
122
|
+
if (this.options.playsinline && !this.isAudio) {
|
|
123
|
+
(this.media as HTMLVideoElement).playsInline = true
|
|
124
|
+
}
|
|
125
|
+
if (this.options.preload) this.media.preload = this.options.preload
|
|
126
|
+
if (this.options.crossorigin) this.media.crossOrigin = this.options.crossorigin
|
|
127
|
+
if (this.options.controlsList?.length) this.media.setAttribute('controlslist', this.options.controlsList.join(' '))
|
|
128
|
+
if (!this.isAudio && this.options.disablePictureInPicture) {
|
|
129
|
+
(this.media as HTMLVideoElement).disablePictureInPicture = true
|
|
130
|
+
}
|
|
131
|
+
if (this.options.poster && !this.isAudio) {
|
|
132
|
+
(this.media as HTMLVideoElement).poster = this.options.poster
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Set initial volume
|
|
136
|
+
if (this.options.volume !== undefined) {
|
|
137
|
+
this.media.volume = this.options.volume
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Append to container
|
|
141
|
+
this.container.appendChild(this.media)
|
|
142
|
+
for (const track of this.options.tracks ?? []) this.addTextTrack(track)
|
|
143
|
+
|
|
144
|
+
// Setup event normalization
|
|
145
|
+
this.eventsNormalizer = new MediaEventsNormalizer(this.media, this.events)
|
|
146
|
+
|
|
147
|
+
// Setup fullscreen listener
|
|
148
|
+
this.cleanupFullscreen = onFullscreenChange((fullscreen) => {
|
|
149
|
+
this.events.emit('fullscreenchange', fullscreen)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Setup PiP listener for video
|
|
153
|
+
if (!this.isAudio && this.media instanceof HTMLVideoElement) {
|
|
154
|
+
this.cleanupPiP = onPiPChange(this.media, (pip) => {
|
|
155
|
+
this.events.emit('pipchange', pip)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Emit state for dimensions and feature availability
|
|
160
|
+
if (!this.isAudio) {
|
|
161
|
+
this.media.addEventListener('loadedmetadata', () => {
|
|
162
|
+
const video = this.media as HTMLVideoElement
|
|
163
|
+
const fullscreenAvailability = this.getFeatureAvailability('fullscreen')
|
|
164
|
+
const pipAvailability = this.getFeatureAvailability('pip')
|
|
165
|
+
const volumeAvailability = this.getFeatureAvailability('volume')
|
|
166
|
+
this.events.emit('statechange', {
|
|
167
|
+
videoWidth: video.videoWidth,
|
|
168
|
+
videoHeight: video.videoHeight,
|
|
169
|
+
aspectRatio: (video.videoHeight > 0 ? video.videoWidth / video.videoHeight : 0) || 16 / 9,
|
|
170
|
+
canFullscreen: fullscreenAvailability === 'available',
|
|
171
|
+
canPictureInPicture: pipAvailability === 'available',
|
|
172
|
+
fullscreenAvailability,
|
|
173
|
+
pipAvailability,
|
|
174
|
+
volumeAvailability,
|
|
175
|
+
})
|
|
176
|
+
this.events.emit('availabilitychange', 'fullscreen', fullscreenAvailability)
|
|
177
|
+
this.events.emit('availabilitychange', 'pip', pipAvailability)
|
|
178
|
+
this.events.emit('availabilitychange', 'volume', volumeAvailability)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Async volume probe after loadeddata
|
|
183
|
+
this.media.addEventListener('loadeddata', () => {
|
|
184
|
+
if (!this.media) return
|
|
185
|
+
probeVolumeAvailability(this.media).then((availability) => {
|
|
186
|
+
this.events.emit('statechange', {
|
|
187
|
+
volumeAvailability: availability,
|
|
188
|
+
})
|
|
189
|
+
this.events.emit('availabilitychange', 'volume', availability)
|
|
190
|
+
}).catch(() => {
|
|
191
|
+
// Volume probe failed — keep current availability state
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// iOS Safari PiP via webkitpresentationmodechanged
|
|
196
|
+
if (!this.isAudio && this.media instanceof HTMLVideoElement) {
|
|
197
|
+
this.media.addEventListener('webkitpresentationmodechanged', () => {
|
|
198
|
+
const video = this.media as any
|
|
199
|
+
if (video?.webkitPresentationMode) {
|
|
200
|
+
const isPiP = video.webkitPresentationMode === 'picture-in-picture'
|
|
201
|
+
this.events.emit('pipchange', isPiP)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected cleanup(): void {
|
|
208
|
+
this.eventsNormalizer?.destroy()
|
|
209
|
+
this.cleanupFullscreen?.()
|
|
210
|
+
this.cleanupPiP?.()
|
|
211
|
+
|
|
212
|
+
if (this.media) {
|
|
213
|
+
this.media.pause()
|
|
214
|
+
this.media.removeAttribute('src')
|
|
215
|
+
this.media.load()
|
|
216
|
+
this.media.remove()
|
|
217
|
+
this.media = null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// === Loading ===
|
|
222
|
+
|
|
223
|
+
canPlay(src: Src): boolean {
|
|
224
|
+
return isHTML5Source(src)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async load(src: Src): Promise<void> {
|
|
228
|
+
if (!this.media) return
|
|
229
|
+
|
|
230
|
+
// Clear existing sources
|
|
231
|
+
this.media.innerHTML = ''
|
|
232
|
+
this.media.removeAttribute('src')
|
|
233
|
+
|
|
234
|
+
// Normalize source
|
|
235
|
+
const sources = Array.isArray(src) ? src : [src]
|
|
236
|
+
|
|
237
|
+
for (const source of sources) {
|
|
238
|
+
if (typeof source === 'string') {
|
|
239
|
+
// Simple URL
|
|
240
|
+
const sourceEl = document.createElement('source')
|
|
241
|
+
sourceEl.src = source
|
|
242
|
+
sourceEl.type = getMimeType(source)
|
|
243
|
+
this.media.appendChild(sourceEl)
|
|
244
|
+
} else if ('src' in source && typeof source.src === 'string') {
|
|
245
|
+
// MediaSource object
|
|
246
|
+
const mediaSource = source as MediaSource
|
|
247
|
+
const sourceEl = document.createElement('source')
|
|
248
|
+
sourceEl.src = mediaSource.src
|
|
249
|
+
sourceEl.type = mediaSource.type || getMimeType(mediaSource.src)
|
|
250
|
+
this.media.appendChild(sourceEl)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Load the media
|
|
255
|
+
this.media.load()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// === Playback ===
|
|
259
|
+
|
|
260
|
+
async play(): Promise<void> {
|
|
261
|
+
if (!this.media) return
|
|
262
|
+
try {
|
|
263
|
+
await this.media.play()
|
|
264
|
+
} catch (error) {
|
|
265
|
+
// Handle autoplay blocked
|
|
266
|
+
if ((error as Error).name === 'NotAllowedError') {
|
|
267
|
+
this.emitError(1, 'Playback was blocked. User interaction required.', error)
|
|
268
|
+
} else {
|
|
269
|
+
throw error
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
pause(): void {
|
|
275
|
+
this.media?.pause()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// === Seeking ===
|
|
279
|
+
|
|
280
|
+
seekTo(time: number): void {
|
|
281
|
+
if (this.media) {
|
|
282
|
+
this.media.currentTime = Math.max(0, Math.min(time, this.getDuration()))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getCurrentTime(): number {
|
|
287
|
+
return this.media?.currentTime || 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
getDuration(): number {
|
|
291
|
+
return this.media?.duration || 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// === Volume ===
|
|
295
|
+
|
|
296
|
+
setVolume(volume: number): void {
|
|
297
|
+
if (this.media) {
|
|
298
|
+
this.media.volume = Math.max(0, Math.min(1, volume))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getVolume(): number {
|
|
303
|
+
return this.media?.volume || 1
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setMuted(muted: boolean): void {
|
|
307
|
+
if (this.media) {
|
|
308
|
+
this.media.muted = muted
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getMuted(): boolean {
|
|
313
|
+
return this.media?.muted || false
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// === Playback Rate ===
|
|
317
|
+
|
|
318
|
+
setPlaybackRate(rate: number): void {
|
|
319
|
+
if (this.media) {
|
|
320
|
+
this.media.playbackRate = Math.max(0.25, Math.min(4, rate))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
getPlaybackRate(): number {
|
|
325
|
+
return this.media?.playbackRate || 1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// === Text Tracks ===
|
|
329
|
+
|
|
330
|
+
getTextTracks(): TextTrack[] {
|
|
331
|
+
if (!this.media) return []
|
|
332
|
+
|
|
333
|
+
const tracks: TextTrack[] = []
|
|
334
|
+
const textTracks = this.media.textTracks
|
|
335
|
+
|
|
336
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
337
|
+
const track = textTracks[i]
|
|
338
|
+
tracks.push({
|
|
339
|
+
id: String(i),
|
|
340
|
+
kind: track.kind as TextTrack['kind'],
|
|
341
|
+
label: track.label || `Track ${i + 1}`,
|
|
342
|
+
language: track.language || '',
|
|
343
|
+
mode: track.mode as TextTrack['mode'],
|
|
344
|
+
cues: this.getTrackCues(track),
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return tracks
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private getTrackCues(track: globalThis.TextTrack): TextTrack['cues'] {
|
|
352
|
+
if (!track.cues) return []
|
|
353
|
+
|
|
354
|
+
const cues: TextTrack['cues'] = []
|
|
355
|
+
for (let i = 0; i < track.cues.length; i++) {
|
|
356
|
+
const cue = track.cues[i] as VTTCue
|
|
357
|
+
cues.push({
|
|
358
|
+
id: cue.id || String(i),
|
|
359
|
+
startTime: cue.startTime,
|
|
360
|
+
endTime: cue.endTime,
|
|
361
|
+
text: cue.text,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
return cues
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
setTextTrackMode(trackId: string, mode: 'disabled' | 'hidden' | 'showing'): void {
|
|
368
|
+
if (!this.media) return
|
|
369
|
+
|
|
370
|
+
const index = parseInt(trackId, 10)
|
|
371
|
+
const track = this.media.textTracks[index]
|
|
372
|
+
if (track) {
|
|
373
|
+
track.mode = mode
|
|
374
|
+
this.events.emit('texttrackchange', mode === 'showing' ? this.getTextTracks()[index] : null)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// === Audio Tracks ===
|
|
379
|
+
|
|
380
|
+
getAudioTracks(): AudioTrack[] {
|
|
381
|
+
if (!this.media || this.isAudio) return []
|
|
382
|
+
|
|
383
|
+
const video = this.media as HTMLVideoElement
|
|
384
|
+
if (!('audioTracks' in video)) return []
|
|
385
|
+
|
|
386
|
+
const audioTracks = (video as any).audioTracks
|
|
387
|
+
const tracks: AudioTrack[] = []
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < audioTracks.length; i++) {
|
|
390
|
+
const track = audioTracks[i]
|
|
391
|
+
tracks.push({
|
|
392
|
+
id: track.id || String(i),
|
|
393
|
+
label: track.label || `Track ${i + 1}`,
|
|
394
|
+
language: track.language || '',
|
|
395
|
+
kind: track.kind || '',
|
|
396
|
+
selected: track.enabled,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return tracks
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
setAudioTrack(trackId: string): void {
|
|
404
|
+
if (!this.media || this.isAudio) return
|
|
405
|
+
|
|
406
|
+
const video = this.media as HTMLVideoElement
|
|
407
|
+
if (!('audioTracks' in video)) return
|
|
408
|
+
|
|
409
|
+
const audioTracks = (video as any).audioTracks
|
|
410
|
+
for (let i = 0; i < audioTracks.length; i++) {
|
|
411
|
+
const isTarget = audioTracks[i].id === trackId || String(i) === trackId
|
|
412
|
+
audioTracks[i].enabled = isTarget
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.events.emit('audiotrackchange', this.getAudioTracks().find((t) => t.selected) || null)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// === Add Text Track ===
|
|
419
|
+
|
|
420
|
+
addTextTrack(options: {
|
|
421
|
+
src?: string
|
|
422
|
+
kind?: TextTrack['kind']
|
|
423
|
+
label?: string
|
|
424
|
+
language?: string
|
|
425
|
+
default?: boolean
|
|
426
|
+
}): void {
|
|
427
|
+
if (!this.media) return
|
|
428
|
+
|
|
429
|
+
const track = document.createElement('track')
|
|
430
|
+
if (options.src) track.src = options.src
|
|
431
|
+
track.kind = options.kind || 'subtitles'
|
|
432
|
+
track.label = options.label || ''
|
|
433
|
+
track.srclang = options.language || ''
|
|
434
|
+
if (options.default) track.default = true
|
|
435
|
+
|
|
436
|
+
this.media.appendChild(track)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* HTML5 Provider Loader
|
|
442
|
+
*/
|
|
443
|
+
export const html5Loader = {
|
|
444
|
+
name: 'html5',
|
|
445
|
+
type: 'video' as ProviderType,
|
|
446
|
+
|
|
447
|
+
canPlay(src: Src): boolean {
|
|
448
|
+
return isHTML5Source(src)
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
mediaType(src: Src): 'video' | 'audio' | 'unknown' {
|
|
452
|
+
if (typeof src === 'string') {
|
|
453
|
+
const extension = getExtension(src)
|
|
454
|
+
if (['mp3', 'wav', 'flac', 'm4a', 'aac', 'oga', 'opus'].includes(extension)) {
|
|
455
|
+
return 'audio'
|
|
456
|
+
}
|
|
457
|
+
return 'video'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (typeof src === 'object' && 'type' in src) {
|
|
461
|
+
const type = (src as MediaSource).type?.toLowerCase() || ''
|
|
462
|
+
if (type.startsWith('audio/')) return 'audio'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return 'video'
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
async load(container: HTMLElement, options: PlayerOptions): Promise<HTML5Provider> {
|
|
469
|
+
const provider = new HTML5Provider()
|
|
470
|
+
await provider.setup(container, options)
|
|
471
|
+
return provider
|
|
472
|
+
},
|
|
473
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Providers
|
|
3
|
+
*
|
|
4
|
+
* Export all media providers.
|
|
5
|
+
*
|
|
6
|
+
* @module providers
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Base
|
|
10
|
+
export { BaseProvider } from './base'
|
|
11
|
+
|
|
12
|
+
// HTML5 Video/Audio
|
|
13
|
+
export { HTML5Provider, html5Loader, isHTML5Source } from './html5'
|
|
14
|
+
|
|
15
|
+
// YouTube
|
|
16
|
+
export { YouTubeProvider, youtubeLoader, isYouTubeSource, extractYouTubeId } from './youtube'
|
|
17
|
+
|
|
18
|
+
// Vimeo
|
|
19
|
+
export { VimeoProvider, vimeoLoader, isVimeoSource, extractVimeoId, extractVimeoHash } from './vimeo'
|
|
20
|
+
|
|
21
|
+
// HLS
|
|
22
|
+
export { HLSProvider, hlsLoader, isHLSSource, isNativeHLSSupported } from './hls'
|
|
23
|
+
|
|
24
|
+
// DASH
|
|
25
|
+
export { DASHProvider, dashLoader, isDASHSource } from './dash'
|
|
26
|
+
|
|
27
|
+
export type { DASHSource, DASHProviderConfig } from './dash'
|
|
28
|
+
export type { DRMConfig } from '../types'
|
|
29
|
+
|
|
30
|
+
// Re-export types
|
|
31
|
+
export type { Provider, ProviderLoader, ProviderType } from '../types'
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Provider Registry
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
import type { ProviderLoader, Src } from '../types'
|
|
38
|
+
import { html5Loader } from './html5'
|
|
39
|
+
import { youtubeLoader } from './youtube'
|
|
40
|
+
import { vimeoLoader } from './vimeo'
|
|
41
|
+
import { hlsLoader } from './hls'
|
|
42
|
+
import { dashLoader } from './dash'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default provider loaders in priority order
|
|
46
|
+
*/
|
|
47
|
+
export const defaultLoaders: ProviderLoader[] = [
|
|
48
|
+
hlsLoader, // Check HLS first
|
|
49
|
+
dashLoader, // Then DASH
|
|
50
|
+
youtubeLoader, // Then YouTube
|
|
51
|
+
vimeoLoader, // Then Vimeo
|
|
52
|
+
html5Loader, // HTML5 as fallback
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find the appropriate provider loader for a source
|
|
57
|
+
*/
|
|
58
|
+
export function findLoader(src: Src, loaders: ProviderLoader[] = defaultLoaders): ProviderLoader | null {
|
|
59
|
+
for (const loader of loaders) {
|
|
60
|
+
if (loader.canPlay(src)) {
|
|
61
|
+
return loader
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SourceCandidate {
|
|
68
|
+
src: Src
|
|
69
|
+
index: number
|
|
70
|
+
loader: ProviderLoader
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve every playable source without discarding ordered fallbacks. */
|
|
74
|
+
export function findSourceCandidates(sources: readonly Src[], loaders: ProviderLoader[] = defaultLoaders): SourceCandidate[] {
|
|
75
|
+
const candidates: SourceCandidate[] = []
|
|
76
|
+
sources.forEach((src, index) => {
|
|
77
|
+
const loader = findLoader(src, loaders)
|
|
78
|
+
if (loader) candidates.push({ src, index, loader })
|
|
79
|
+
})
|
|
80
|
+
return candidates
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect media type from source
|
|
85
|
+
*/
|
|
86
|
+
export function detectMediaType(src: Src, loaders: ProviderLoader[] = defaultLoaders): 'video' | 'audio' | 'unknown' {
|
|
87
|
+
const loader = findLoader(src, loaders)
|
|
88
|
+
return loader?.mediaType(src) || 'unknown'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get preconnect hints for a source
|
|
93
|
+
*/
|
|
94
|
+
export function getPreconnectHints(src: Src, loaders: ProviderLoader[] = defaultLoaders): string[] {
|
|
95
|
+
const loader = findLoader(src, loaders)
|
|
96
|
+
return loader?.preconnect?.() || []
|
|
97
|
+
}
|