@livepeer-frameworks/player-core 0.0.3
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/dist/cjs/index.js +19493 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +19398 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/player.css +2140 -0
- package/dist/types/core/ABRController.d.ts +164 -0
- package/dist/types/core/CodecUtils.d.ts +54 -0
- package/dist/types/core/Disposable.d.ts +61 -0
- package/dist/types/core/EventEmitter.d.ts +73 -0
- package/dist/types/core/GatewayClient.d.ts +144 -0
- package/dist/types/core/InteractionController.d.ts +121 -0
- package/dist/types/core/LiveDurationProxy.d.ts +102 -0
- package/dist/types/core/MetaTrackManager.d.ts +220 -0
- package/dist/types/core/MistReporter.d.ts +163 -0
- package/dist/types/core/MistSignaling.d.ts +148 -0
- package/dist/types/core/PlayerController.d.ts +665 -0
- package/dist/types/core/PlayerInterface.d.ts +230 -0
- package/dist/types/core/PlayerManager.d.ts +182 -0
- package/dist/types/core/PlayerRegistry.d.ts +27 -0
- package/dist/types/core/QualityMonitor.d.ts +184 -0
- package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
- package/dist/types/core/SeekingUtils.d.ts +142 -0
- package/dist/types/core/StreamStateClient.d.ts +108 -0
- package/dist/types/core/SubtitleManager.d.ts +111 -0
- package/dist/types/core/TelemetryReporter.d.ts +79 -0
- package/dist/types/core/TimeFormat.d.ts +97 -0
- package/dist/types/core/TimerManager.d.ts +83 -0
- package/dist/types/core/UrlUtils.d.ts +81 -0
- package/dist/types/core/detector.d.ts +149 -0
- package/dist/types/core/index.d.ts +49 -0
- package/dist/types/core/scorer.d.ts +167 -0
- package/dist/types/core/selector.d.ts +9 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/players/DashJsPlayer.d.ts +102 -0
- package/dist/types/players/HlsJsPlayer.d.ts +70 -0
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
- package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
- package/dist/types/players/MistPlayer.d.ts +25 -0
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
- package/dist/types/players/NativePlayer.d.ts +143 -0
- package/dist/types/players/VideoJsPlayer.d.ts +59 -0
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
- package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
- package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
- package/dist/types/players/index.d.ts +14 -0
- package/dist/types/styles/index.d.ts +11 -0
- package/dist/types/types.d.ts +363 -0
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
- package/dist/types/vanilla/index.d.ts +19 -0
- package/dist/workers/decoder.worker.js +989 -0
- package/dist/workers/decoder.worker.js.map +1 -0
- package/package.json +80 -0
- package/src/core/ABRController.ts +550 -0
- package/src/core/CodecUtils.ts +257 -0
- package/src/core/Disposable.ts +120 -0
- package/src/core/EventEmitter.ts +113 -0
- package/src/core/GatewayClient.ts +439 -0
- package/src/core/InteractionController.ts +712 -0
- package/src/core/LiveDurationProxy.ts +270 -0
- package/src/core/MetaTrackManager.ts +753 -0
- package/src/core/MistReporter.ts +543 -0
- package/src/core/MistSignaling.ts +346 -0
- package/src/core/PlayerController.ts +2829 -0
- package/src/core/PlayerInterface.ts +432 -0
- package/src/core/PlayerManager.ts +900 -0
- package/src/core/PlayerRegistry.ts +149 -0
- package/src/core/QualityMonitor.ts +597 -0
- package/src/core/ScreenWakeLockManager.ts +163 -0
- package/src/core/SeekingUtils.ts +364 -0
- package/src/core/StreamStateClient.ts +457 -0
- package/src/core/SubtitleManager.ts +297 -0
- package/src/core/TelemetryReporter.ts +308 -0
- package/src/core/TimeFormat.ts +205 -0
- package/src/core/TimerManager.ts +209 -0
- package/src/core/UrlUtils.ts +179 -0
- package/src/core/detector.ts +382 -0
- package/src/core/index.ts +140 -0
- package/src/core/scorer.ts +553 -0
- package/src/core/selector.ts +16 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/players/DashJsPlayer.ts +642 -0
- package/src/players/HlsJsPlayer.ts +483 -0
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
- package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
- package/src/players/MewsWsPlayer/index.ts +1065 -0
- package/src/players/MewsWsPlayer/types.ts +106 -0
- package/src/players/MistPlayer.ts +188 -0
- package/src/players/MistWebRTCPlayer/index.ts +703 -0
- package/src/players/NativePlayer.ts +820 -0
- package/src/players/VideoJsPlayer.ts +643 -0
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
- package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
- package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
- package/src/players/WebCodecsPlayer/index.ts +1650 -0
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
- package/src/players/WebCodecsPlayer/types.ts +542 -0
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
- package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
- package/src/players/index.ts +22 -0
- package/src/styles/animations.css +21 -0
- package/src/styles/index.ts +52 -0
- package/src/styles/player.css +2126 -0
- package/src/styles/tailwind.css +1015 -0
- package/src/types.ts +421 -0
- package/src/vanilla/FrameWorksPlayer.ts +367 -0
- package/src/vanilla/index.ts +22 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { BasePlayer } from '../core/PlayerInterface';
|
|
2
|
+
import { checkProtocolMismatch, getBrowserInfo } from '../core/detector';
|
|
3
|
+
import { translateCodec } from '../core/CodecUtils';
|
|
4
|
+
import { LiveDurationProxy } from '../core/LiveDurationProxy';
|
|
5
|
+
import type { StreamSource, StreamInfo, PlayerOptions, PlayerCapability } from '../core/PlayerInterface';
|
|
6
|
+
|
|
7
|
+
// Player implementation class
|
|
8
|
+
export class HlsJsPlayerImpl extends BasePlayer {
|
|
9
|
+
readonly capability: PlayerCapability = {
|
|
10
|
+
name: "HLS.js Player",
|
|
11
|
+
shortname: "hlsjs",
|
|
12
|
+
priority: 3,
|
|
13
|
+
mimes: ["html5/application/vnd.apple.mpegurl", "html5/application/vnd.apple.mpegurl;version=7"]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
private hls: any = null;
|
|
17
|
+
private container: HTMLElement | null = null;
|
|
18
|
+
private failureCount = 0;
|
|
19
|
+
private destroyed = false;
|
|
20
|
+
private liveDurationProxy: LiveDurationProxy | null = null;
|
|
21
|
+
|
|
22
|
+
isMimeSupported(mimetype: string): boolean {
|
|
23
|
+
return this.capability.mimes.includes(mimetype);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isBrowserSupported(mimetype: string, source: StreamSource, streamInfo: StreamInfo): boolean | string[] {
|
|
27
|
+
// Check protocol mismatch
|
|
28
|
+
if (checkProtocolMismatch(source.url)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if HLS.js is supported or native HLS is available
|
|
33
|
+
const browser = getBrowserInfo();
|
|
34
|
+
|
|
35
|
+
// If native HLS is supported (Safari/iOS), prefer that for older Android
|
|
36
|
+
if (browser.isAndroid && browser.isMobile) {
|
|
37
|
+
// Let VideoJS handle older Android instead
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check MediaSource support (required for HLS.js)
|
|
42
|
+
if (!browser.supportsMediaSource) {
|
|
43
|
+
// Fall back to native if available
|
|
44
|
+
const testVideo = document.createElement('video');
|
|
45
|
+
if (testVideo.canPlayType('application/vnd.apple.mpegurl')) {
|
|
46
|
+
return ['video', 'audio'];
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check codec compatibility
|
|
52
|
+
const playableTracks: string[] = [];
|
|
53
|
+
const tracksByType: Record<string, typeof streamInfo.meta.tracks> = {};
|
|
54
|
+
|
|
55
|
+
// If no track info available yet, assume compatible (like upstream does)
|
|
56
|
+
// Track info comes async from MistServer - don't block on it
|
|
57
|
+
if (!streamInfo.meta.tracks || streamInfo.meta.tracks.length === 0) {
|
|
58
|
+
return ['video', 'audio']; // Assume standard tracks until we know better
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Group tracks by type
|
|
62
|
+
for (const track of streamInfo.meta.tracks) {
|
|
63
|
+
if (track.type === 'meta') {
|
|
64
|
+
if (track.codec === 'subtitle') {
|
|
65
|
+
// Check for WebVTT subtitle support
|
|
66
|
+
for (const src of streamInfo.source) {
|
|
67
|
+
if (src.type === 'html5/text/vtt') {
|
|
68
|
+
playableTracks.push('subtitle');
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!tracksByType[track.type]) {
|
|
77
|
+
tracksByType[track.type] = [];
|
|
78
|
+
}
|
|
79
|
+
tracksByType[track.type].push(track);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// HLS-incompatible audio codecs (even if browser MSE supports them in fMP4)
|
|
83
|
+
// HLS standard only supports: AAC, MP3, AC-3/E-AC-3
|
|
84
|
+
const HLS_INCOMPATIBLE_AUDIO = ['OPUS', 'Opus', 'opus', 'VORBIS', 'Vorbis', 'FLAC'];
|
|
85
|
+
|
|
86
|
+
// Test codec support for video/audio tracks
|
|
87
|
+
for (const [trackType, tracks] of Object.entries(tracksByType)) {
|
|
88
|
+
let hasPlayableTrack = false;
|
|
89
|
+
|
|
90
|
+
for (const track of tracks) {
|
|
91
|
+
// Explicit HLS codec filtering - OPUS doesn't work in HLS even if MSE supports it
|
|
92
|
+
if (trackType === 'audio' && HLS_INCOMPATIBLE_AUDIO.includes(track.codec)) {
|
|
93
|
+
console.debug(`[HLS.js] Codec incompatible with HLS: ${track.codec}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const codecString = translateCodec(track);
|
|
98
|
+
// Use correct container type for audio vs video tracks
|
|
99
|
+
const container = trackType === 'audio' ? 'audio/mp4' : 'video/mp4';
|
|
100
|
+
const mimeType = `${container};codecs="${codecString}"`;
|
|
101
|
+
|
|
102
|
+
if (MediaSource.isTypeSupported && MediaSource.isTypeSupported(mimeType)) {
|
|
103
|
+
hasPlayableTrack = true;
|
|
104
|
+
break;
|
|
105
|
+
} else {
|
|
106
|
+
console.debug(`[HLS.js] Codec not supported: ${mimeType}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (hasPlayableTrack) {
|
|
111
|
+
playableTracks.push(trackType);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return playableTracks.length > 0 ? playableTracks : false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions): Promise<HTMLVideoElement> {
|
|
119
|
+
console.log('[HLS.js] initialize() starting for', source.url.slice(0, 60) + '...');
|
|
120
|
+
this.destroyed = false;
|
|
121
|
+
this.container = container;
|
|
122
|
+
container.classList.add('fw-player-container');
|
|
123
|
+
|
|
124
|
+
// Create video element
|
|
125
|
+
const video = document.createElement('video');
|
|
126
|
+
video.classList.add('fw-player-video');
|
|
127
|
+
video.setAttribute('playsinline', '');
|
|
128
|
+
video.setAttribute('crossorigin', 'anonymous');
|
|
129
|
+
|
|
130
|
+
// Apply options
|
|
131
|
+
if (options.autoplay) video.autoplay = true;
|
|
132
|
+
if (options.muted) video.muted = true;
|
|
133
|
+
video.controls = options.controls === true; // Explicit false to hide native controls
|
|
134
|
+
if (options.loop) video.loop = true;
|
|
135
|
+
if (options.poster) video.poster = options.poster;
|
|
136
|
+
|
|
137
|
+
this.videoElement = video;
|
|
138
|
+
container.appendChild(video);
|
|
139
|
+
|
|
140
|
+
// Set up event listeners
|
|
141
|
+
this.setupVideoEventListeners(video, options);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Dynamic import of HLS.js
|
|
145
|
+
console.log('[HLS.js] Dynamically importing hls.js module...');
|
|
146
|
+
const mod = await import('hls.js');
|
|
147
|
+
const Hls = (mod as any).default || (mod as any);
|
|
148
|
+
console.log('[HLS.js] hls.js module imported, Hls.isSupported():', Hls.isSupported?.());
|
|
149
|
+
|
|
150
|
+
if (Hls.isSupported()) {
|
|
151
|
+
this.hls = new Hls({
|
|
152
|
+
enableWorker: false,
|
|
153
|
+
lowLatencyMode: true,
|
|
154
|
+
maxBufferLength: 15,
|
|
155
|
+
maxMaxBufferLength: 60,
|
|
156
|
+
backBufferLength: 30 // Reduced from 90 to prevent memory issues on long streams
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.hls.attachMedia(video);
|
|
160
|
+
|
|
161
|
+
this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
162
|
+
this.hls.loadSource(source.url);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.hls.on(Hls.Events.ERROR, (_: any, data: any) => {
|
|
166
|
+
if (this.destroyed) return; // Guard against zombie callbacks
|
|
167
|
+
if (data?.fatal) {
|
|
168
|
+
if (this.failureCount < 3) {
|
|
169
|
+
this.failureCount++;
|
|
170
|
+
try { this.hls.recoverMediaError(); } catch {}
|
|
171
|
+
} else {
|
|
172
|
+
const error = `HLS fatal error: ${data?.type || 'unknown'}`;
|
|
173
|
+
this.emit('error', error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
179
|
+
if (this.destroyed) return; // Guard against zombie callbacks
|
|
180
|
+
|
|
181
|
+
// Set up LiveDurationProxy for live streams
|
|
182
|
+
// HLS.js sets video.duration to Infinity for live streams
|
|
183
|
+
const isLive = !isFinite(video.duration) || this.hls.levels?.[0]?.details?.live;
|
|
184
|
+
if (isLive && !this.liveDurationProxy) {
|
|
185
|
+
this.liveDurationProxy = new LiveDurationProxy(video, {
|
|
186
|
+
constrainSeek: true,
|
|
187
|
+
liveOffset: 0,
|
|
188
|
+
});
|
|
189
|
+
console.debug('[HLS.js] LiveDurationProxy initialized for live stream');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.autoplay) {
|
|
193
|
+
video.play().catch(e => console.warn('HLS autoplay failed:', e));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
198
|
+
// Use native HLS support
|
|
199
|
+
video.src = source.url;
|
|
200
|
+
if (options.autoplay) {
|
|
201
|
+
video.play().catch(e => console.warn('Native HLS autoplay failed:', e));
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
throw new Error('HLS not supported in this browser');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Optional subtitle tracks helper from source extras
|
|
208
|
+
try {
|
|
209
|
+
const subs = (source as any).subtitles as Array<{ label: string; lang: string; src: string }>;
|
|
210
|
+
if (Array.isArray(subs)) {
|
|
211
|
+
subs.forEach((s, idx) => {
|
|
212
|
+
const track = document.createElement('track');
|
|
213
|
+
track.kind = 'subtitles';
|
|
214
|
+
track.label = s.label;
|
|
215
|
+
track.srclang = s.lang;
|
|
216
|
+
track.src = s.src;
|
|
217
|
+
if (idx === 0) track.default = true;
|
|
218
|
+
video.appendChild(track);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
|
|
223
|
+
console.log('[HLS.js] initialize() complete, returning video element');
|
|
224
|
+
return video;
|
|
225
|
+
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
this.emit('error', error.message || String(error));
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async destroy(): Promise<void> {
|
|
233
|
+
console.debug('[HLS.js] destroy() called');
|
|
234
|
+
this.destroyed = true;
|
|
235
|
+
|
|
236
|
+
if (this.liveDurationProxy) {
|
|
237
|
+
this.liveDurationProxy.destroy();
|
|
238
|
+
this.liveDurationProxy = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (this.hls) {
|
|
242
|
+
try {
|
|
243
|
+
this.hls.destroy();
|
|
244
|
+
console.debug('[HLS.js] hls.destroy() completed');
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn('[HLS.js] Error destroying:', e);
|
|
247
|
+
}
|
|
248
|
+
this.hls = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.videoElement && this.container) {
|
|
252
|
+
try { this.container.removeChild(this.videoElement); } catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.videoElement = null;
|
|
256
|
+
this.container = null;
|
|
257
|
+
this.listeners.clear();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Live Stream Support
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the calculated duration for live streams
|
|
266
|
+
* Falls back to native duration for VOD
|
|
267
|
+
*/
|
|
268
|
+
getDuration(): number {
|
|
269
|
+
if (this.liveDurationProxy) {
|
|
270
|
+
return this.liveDurationProxy.getDuration();
|
|
271
|
+
}
|
|
272
|
+
return this.videoElement?.duration ?? 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if the stream is live
|
|
277
|
+
*/
|
|
278
|
+
isLiveStream(): boolean {
|
|
279
|
+
if (this.liveDurationProxy) {
|
|
280
|
+
return this.liveDurationProxy.isLive();
|
|
281
|
+
}
|
|
282
|
+
const video = this.videoElement;
|
|
283
|
+
if (!video) return false;
|
|
284
|
+
return !isFinite(video.duration);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Seek to a position with live-aware constraints
|
|
289
|
+
*/
|
|
290
|
+
seek(time: number): void {
|
|
291
|
+
const video = this.videoElement;
|
|
292
|
+
if (!video) return;
|
|
293
|
+
|
|
294
|
+
// For live streams, use the proxy which constrains to buffer
|
|
295
|
+
if (this.liveDurationProxy && this.liveDurationProxy.isLive()) {
|
|
296
|
+
this.liveDurationProxy.seek(time);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// For VOD, seek directly
|
|
301
|
+
video.currentTime = time;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Jump to live edge
|
|
306
|
+
* Uses HLS.js liveSyncPosition when available (more accurate)
|
|
307
|
+
*/
|
|
308
|
+
jumpToLive(): void {
|
|
309
|
+
const video = this.videoElement;
|
|
310
|
+
if (!video) return;
|
|
311
|
+
|
|
312
|
+
// HLS.js provides liveSyncPosition for live streams - use that first
|
|
313
|
+
if (this.hls && typeof this.hls.liveSyncPosition === 'number' && this.hls.liveSyncPosition > 0) {
|
|
314
|
+
console.debug('[HLS.js] jumpToLive using liveSyncPosition:', this.hls.liveSyncPosition);
|
|
315
|
+
video.currentTime = this.hls.liveSyncPosition;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Fall back to LiveDurationProxy
|
|
320
|
+
if (this.liveDurationProxy && this.liveDurationProxy.isLive()) {
|
|
321
|
+
console.debug('[HLS.js] jumpToLive using LiveDurationProxy');
|
|
322
|
+
this.liveDurationProxy.jumpToLive();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Last resort: use seekable end
|
|
327
|
+
if (video.seekable && video.seekable.length > 0) {
|
|
328
|
+
const liveEdge = video.seekable.end(video.seekable.length - 1);
|
|
329
|
+
if (isFinite(liveEdge) && liveEdge > 0) {
|
|
330
|
+
console.debug('[HLS.js] jumpToLive using seekable.end:', liveEdge);
|
|
331
|
+
video.currentTime = liveEdge;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Provide a seekable range override for live streams.
|
|
338
|
+
* Uses liveSyncPosition as the live edge to avoid waiting for the absolute end.
|
|
339
|
+
*/
|
|
340
|
+
getSeekableRange(): { start: number; end: number } | null {
|
|
341
|
+
const video = this.videoElement;
|
|
342
|
+
if (!video?.seekable || video.seekable.length === 0) return null;
|
|
343
|
+
const start = video.seekable.start(0);
|
|
344
|
+
let end = video.seekable.end(video.seekable.length - 1);
|
|
345
|
+
|
|
346
|
+
if (this.liveDurationProxy?.isLive() && this.hls && typeof this.hls.liveSyncPosition === 'number') {
|
|
347
|
+
const sync = this.hls.liveSyncPosition;
|
|
348
|
+
if (Number.isFinite(sync) && sync > 0) {
|
|
349
|
+
end = Math.min(end, sync);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
|
|
354
|
+
return { start, end };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get latency from live edge (for live streams)
|
|
359
|
+
*/
|
|
360
|
+
getLiveLatency(): number {
|
|
361
|
+
const video = this.videoElement;
|
|
362
|
+
if (!video) return 0;
|
|
363
|
+
|
|
364
|
+
// HLS.js provides liveSyncPosition
|
|
365
|
+
if (this.hls && typeof this.hls.liveSyncPosition === 'number') {
|
|
366
|
+
return Math.max(0, (this.hls.liveSyncPosition - video.currentTime) * 1000);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Fall back to proxy
|
|
370
|
+
if (this.liveDurationProxy) {
|
|
371
|
+
return this.liveDurationProxy.getLatency() * 1000;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Quality API (Auto + levels)
|
|
379
|
+
// ============================================================================
|
|
380
|
+
getQualities(): Array<{ id: string; label: string; bitrate?: number; width?: number; height?: number; isAuto?: boolean; active?: boolean }> {
|
|
381
|
+
const qualities: any[] = [];
|
|
382
|
+
const video = this.videoElement;
|
|
383
|
+
if (!this.hls || !video) return qualities;
|
|
384
|
+
const levels = this.hls.levels || [];
|
|
385
|
+
const auto = { id: 'auto', label: 'Auto', isAuto: true, active: this.hls.autoLevelEnabled };
|
|
386
|
+
qualities.push(auto);
|
|
387
|
+
levels.forEach((lvl: any, idx: number) => {
|
|
388
|
+
qualities.push({ id: String(idx), label: lvl.height ? `${lvl.height}p` : `${Math.round((lvl.bitrate||0)/1000)}kbps`, bitrate: lvl.bitrate, width: lvl.width, height: lvl.height, active: this.hls.currentLevel === idx });
|
|
389
|
+
});
|
|
390
|
+
return qualities;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
selectQuality(id: string): void {
|
|
394
|
+
if (!this.hls) return;
|
|
395
|
+
if (id === 'auto') {
|
|
396
|
+
this.hls.currentLevel = -1;
|
|
397
|
+
this.hls.autoLevelEnabled = true;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const idx = parseInt(id, 10);
|
|
401
|
+
if (!isNaN(idx)) {
|
|
402
|
+
this.hls.autoLevelEnabled = false;
|
|
403
|
+
this.hls.currentLevel = idx;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Captions via native textTracks if rendered; hls.js can also manage subtitles tracks
|
|
408
|
+
getTextTracks(): Array<{ id: string; label: string; lang?: string; active: boolean }> {
|
|
409
|
+
const v = this.videoElement;
|
|
410
|
+
if (!v) return [];
|
|
411
|
+
const list = v.textTracks;
|
|
412
|
+
const out: any[] = [];
|
|
413
|
+
for (let i = 0; i < list.length; i++) {
|
|
414
|
+
const tt = list[i];
|
|
415
|
+
out.push({ id: String(i), label: tt.label || `CC ${i+1}`, lang: (tt as any).language, active: tt.mode === 'showing' });
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
selectTextTrack(id: string | null): void {
|
|
421
|
+
const v = this.videoElement as any;
|
|
422
|
+
if (!v) return;
|
|
423
|
+
const list = v.textTracks as TextTrackList;
|
|
424
|
+
for (let i = 0; i < list.length; i++) {
|
|
425
|
+
const tt = list[i];
|
|
426
|
+
if (id !== null && String(i) === id) tt.mode = 'showing'; else tt.mode = 'disabled';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get HLS.js-specific stats for accurate bitrate and bandwidth
|
|
432
|
+
*/
|
|
433
|
+
async getStats(): Promise<{
|
|
434
|
+
type: 'hls';
|
|
435
|
+
bandwidthEstimate: number;
|
|
436
|
+
currentLevel: number;
|
|
437
|
+
currentBitrate: number;
|
|
438
|
+
loadLevel: number;
|
|
439
|
+
levels: Array<{ bitrate: number; width: number; height: number }>;
|
|
440
|
+
buffered: number;
|
|
441
|
+
latency?: number;
|
|
442
|
+
} | undefined> {
|
|
443
|
+
if (!this.hls) return undefined;
|
|
444
|
+
|
|
445
|
+
const levels = (this.hls.levels || []).map((lvl: any) => ({
|
|
446
|
+
bitrate: lvl.bitrate || 0,
|
|
447
|
+
width: lvl.width || 0,
|
|
448
|
+
height: lvl.height || 0,
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
const currentLevel = this.hls.currentLevel;
|
|
452
|
+
const currentLevelData = levels[currentLevel];
|
|
453
|
+
|
|
454
|
+
// Calculate buffered ahead
|
|
455
|
+
let buffered = 0;
|
|
456
|
+
const video = this.videoElement;
|
|
457
|
+
if (video && video.buffered.length > 0) {
|
|
458
|
+
for (let i = 0; i < video.buffered.length; i++) {
|
|
459
|
+
if (video.buffered.start(i) <= video.currentTime && video.buffered.end(i) > video.currentTime) {
|
|
460
|
+
buffered = video.buffered.end(i) - video.currentTime;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Latency for live streams
|
|
467
|
+
let latency: number | undefined;
|
|
468
|
+
if (video && this.hls.liveSyncPosition !== undefined && !isFinite(video.duration)) {
|
|
469
|
+
latency = (this.hls.liveSyncPosition - video.currentTime) * 1000;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
type: 'hls',
|
|
474
|
+
bandwidthEstimate: this.hls.bandwidthEstimate || 0,
|
|
475
|
+
currentLevel,
|
|
476
|
+
currentBitrate: currentLevelData?.bitrate || 0,
|
|
477
|
+
loadLevel: this.hls.loadLevel || 0,
|
|
478
|
+
levels,
|
|
479
|
+
buffered,
|
|
480
|
+
latency,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|