@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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenWakeLockManager - Prevents device sleep during video playback
|
|
3
|
+
*
|
|
4
|
+
* Uses the Screen Wake Lock API to keep the screen awake during:
|
|
5
|
+
* - Fullscreen video playback
|
|
6
|
+
* - Active video playback (optional)
|
|
7
|
+
*
|
|
8
|
+
* Gracefully falls back to no-op on unsupported browsers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ScreenWakeLockConfig {
|
|
12
|
+
/** Acquire wake lock on any playback, not just fullscreen (default: false) */
|
|
13
|
+
acquireOnPlay?: boolean;
|
|
14
|
+
/** Callback when wake lock is acquired */
|
|
15
|
+
onAcquire?: () => void;
|
|
16
|
+
/** Callback when wake lock is released */
|
|
17
|
+
onRelease?: () => void;
|
|
18
|
+
/** Callback on error */
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ScreenWakeLockManager {
|
|
23
|
+
private wakeLock: WakeLockSentinel | null = null;
|
|
24
|
+
private config: ScreenWakeLockConfig;
|
|
25
|
+
private isSupported: boolean;
|
|
26
|
+
private isPlaying = false;
|
|
27
|
+
private isFullscreen = false;
|
|
28
|
+
private isDestroyed = false;
|
|
29
|
+
|
|
30
|
+
// Bound handlers for visibility change
|
|
31
|
+
private boundVisibilityChange: () => void;
|
|
32
|
+
|
|
33
|
+
constructor(config: ScreenWakeLockConfig = {}) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.isSupported = 'wakeLock' in navigator;
|
|
36
|
+
|
|
37
|
+
this.boundVisibilityChange = this.handleVisibilityChange.bind(this);
|
|
38
|
+
|
|
39
|
+
// Re-acquire wake lock when page becomes visible again
|
|
40
|
+
if (this.isSupported) {
|
|
41
|
+
document.addEventListener('visibilitychange', this.boundVisibilityChange);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if Screen Wake Lock API is supported
|
|
47
|
+
*/
|
|
48
|
+
static isSupported(): boolean {
|
|
49
|
+
return 'wakeLock' in navigator;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update playing state
|
|
54
|
+
*/
|
|
55
|
+
setPlaying(isPlaying: boolean): void {
|
|
56
|
+
if (this.isDestroyed) return;
|
|
57
|
+
|
|
58
|
+
this.isPlaying = isPlaying;
|
|
59
|
+
this.updateWakeLock();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update fullscreen state
|
|
64
|
+
*/
|
|
65
|
+
setFullscreen(isFullscreen: boolean): void {
|
|
66
|
+
if (this.isDestroyed) return;
|
|
67
|
+
|
|
68
|
+
this.isFullscreen = isFullscreen;
|
|
69
|
+
this.updateWakeLock();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if wake lock is currently held
|
|
74
|
+
*/
|
|
75
|
+
isHeld(): boolean {
|
|
76
|
+
return this.wakeLock !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Manually acquire wake lock
|
|
81
|
+
*/
|
|
82
|
+
async acquire(): Promise<void> {
|
|
83
|
+
if (this.isDestroyed) return;
|
|
84
|
+
if (!this.isSupported) return;
|
|
85
|
+
if (this.wakeLock) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
this.wakeLock = await navigator.wakeLock.request('screen');
|
|
89
|
+
this.wakeLock.addEventListener('release', this.handleRelease.bind(this));
|
|
90
|
+
this.config.onAcquire?.();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Wake lock request can fail if:
|
|
93
|
+
// - Document is not visible
|
|
94
|
+
// - Low battery mode
|
|
95
|
+
// - Permission denied
|
|
96
|
+
this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Manually release wake lock
|
|
102
|
+
*/
|
|
103
|
+
release(): void {
|
|
104
|
+
if (this.wakeLock) {
|
|
105
|
+
this.wakeLock.release().catch(() => {
|
|
106
|
+
// Ignore release errors
|
|
107
|
+
});
|
|
108
|
+
this.wakeLock = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Destroy the manager and release wake lock
|
|
114
|
+
*/
|
|
115
|
+
destroy(): void {
|
|
116
|
+
if (this.isDestroyed) return;
|
|
117
|
+
this.isDestroyed = true;
|
|
118
|
+
|
|
119
|
+
this.release();
|
|
120
|
+
|
|
121
|
+
if (this.isSupported) {
|
|
122
|
+
document.removeEventListener('visibilitychange', this.boundVisibilityChange);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update wake lock based on current state
|
|
128
|
+
*/
|
|
129
|
+
private updateWakeLock(): void {
|
|
130
|
+
const shouldHold =
|
|
131
|
+
this.isPlaying && (this.isFullscreen || this.config.acquireOnPlay);
|
|
132
|
+
|
|
133
|
+
if (shouldHold && !this.wakeLock) {
|
|
134
|
+
this.acquire();
|
|
135
|
+
} else if (!shouldHold && this.wakeLock) {
|
|
136
|
+
this.release();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle wake lock release event
|
|
142
|
+
*/
|
|
143
|
+
private handleRelease(): void {
|
|
144
|
+
this.wakeLock = null;
|
|
145
|
+
this.config.onRelease?.();
|
|
146
|
+
|
|
147
|
+
// Try to re-acquire if conditions still met
|
|
148
|
+
if (!this.isDestroyed) {
|
|
149
|
+
this.updateWakeLock();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle visibility change - re-acquire if page becomes visible
|
|
155
|
+
*/
|
|
156
|
+
private handleVisibilityChange(): void {
|
|
157
|
+
if (document.visibilityState === 'visible' && !this.wakeLock) {
|
|
158
|
+
this.updateWakeLock();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default ScreenWakeLockManager;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeekingUtils.ts
|
|
3
|
+
*
|
|
4
|
+
* Centralized seeking and live detection logic for player controls.
|
|
5
|
+
* Used by React, Svelte, and Vanilla wrappers to ensure consistent behavior.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Seekable range: The portion of the stream that can be seeked to
|
|
9
|
+
* - Live edge: The furthest point in time that can be played (live point)
|
|
10
|
+
* - Near live: Whether playback is close enough to live edge to show "LIVE" badge
|
|
11
|
+
* - Latency tier: Protocol-based classification affecting live detection thresholds
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { MistStreamInfo, MistTrackInfo } from '../types';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export type LatencyTier = 'ultra-low' | 'low' | 'medium' | 'high';
|
|
21
|
+
|
|
22
|
+
export interface LiveThresholds {
|
|
23
|
+
/** Seconds behind live edge to exit "LIVE" state (become clickable) */
|
|
24
|
+
exitLive: number;
|
|
25
|
+
/** Seconds behind live edge to enter "LIVE" state (become non-clickable) */
|
|
26
|
+
enterLive: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SeekableRange {
|
|
30
|
+
/** Start of seekable range in seconds */
|
|
31
|
+
seekableStart: number;
|
|
32
|
+
/** End of seekable range (live edge) in seconds */
|
|
33
|
+
liveEdge: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SeekableRangeParams {
|
|
37
|
+
isLive: boolean;
|
|
38
|
+
video: HTMLVideoElement | null;
|
|
39
|
+
mistStreamInfo?: MistStreamInfo;
|
|
40
|
+
currentTime: number;
|
|
41
|
+
duration: number;
|
|
42
|
+
/** Allow Mist track metadata for MediaStream sources (e.g., WebCodecs DVR) */
|
|
43
|
+
allowMediaStreamDvr?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CanSeekParams {
|
|
47
|
+
video: HTMLVideoElement | null;
|
|
48
|
+
isLive: boolean;
|
|
49
|
+
duration: number;
|
|
50
|
+
bufferWindowMs?: number;
|
|
51
|
+
playerCanSeek?: () => boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Constants
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Latency tier thresholds for "near live" detection.
|
|
60
|
+
* Different protocols have vastly different latency expectations.
|
|
61
|
+
*
|
|
62
|
+
* exitLive: How far behind (seconds) before we show "behind live" indicator
|
|
63
|
+
* enterLive: How close to live (seconds) before we show "LIVE" badge again
|
|
64
|
+
*
|
|
65
|
+
* The gap between exitLive and enterLive creates hysteresis to prevent flicker.
|
|
66
|
+
*/
|
|
67
|
+
export const LATENCY_TIERS: Record<LatencyTier, LiveThresholds> = {
|
|
68
|
+
// WebRTC/WHEP: sub-second latency
|
|
69
|
+
'ultra-low': { exitLive: 2, enterLive: 0.5 },
|
|
70
|
+
// MEWS (WebSocket MP4): 2-5s latency
|
|
71
|
+
'low': { exitLive: 5, enterLive: 1.5 },
|
|
72
|
+
// HLS/DASH: 10-30s latency (segment-based)
|
|
73
|
+
'medium': { exitLive: 15, enterLive: 5 },
|
|
74
|
+
// Fallback for unknown protocols
|
|
75
|
+
'high': { exitLive: 30, enterLive: 10 },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Playback speed presets for UI controls.
|
|
80
|
+
*/
|
|
81
|
+
export const SPEED_PRESETS = [0.5, 1, 1.5, 2] as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Default fallback buffer window when no other info available (in seconds).
|
|
85
|
+
* Aligned with MistServer reference player's 60-second default.
|
|
86
|
+
*/
|
|
87
|
+
export const DEFAULT_BUFFER_WINDOW_SEC = 60;
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Pure Functions
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect latency tier from source type string.
|
|
95
|
+
*
|
|
96
|
+
* @param sourceType - MIME type or protocol identifier (e.g., 'whep', 'ws/video/mp4')
|
|
97
|
+
* @returns Latency tier classification
|
|
98
|
+
*/
|
|
99
|
+
export function getLatencyTier(sourceType?: string): LatencyTier {
|
|
100
|
+
if (!sourceType) return 'medium';
|
|
101
|
+
const t = sourceType.toLowerCase();
|
|
102
|
+
|
|
103
|
+
// Ultra-low: WebRTC protocols (sub-second latency)
|
|
104
|
+
if (t === 'whep' || t === 'webrtc' || t.includes('mist/webrtc')) {
|
|
105
|
+
return 'ultra-low';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Low: WebSocket-based streaming (2-5s latency)
|
|
109
|
+
if (t.startsWith('ws/') || t.startsWith('wss/')) {
|
|
110
|
+
return 'low';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Medium: HLS/DASH (segment-based, 10-30s latency)
|
|
114
|
+
if (t.includes('mpegurl') || t.includes('dash')) {
|
|
115
|
+
return 'medium';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Progressive MP4/WebM - use medium defaults
|
|
119
|
+
if (t.includes('video/mp4') || t.includes('video/webm')) {
|
|
120
|
+
return 'medium';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 'medium';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if video element is using WebRTC/MediaStream source.
|
|
128
|
+
* WebRTC streams have special constraints (no seeking, no playback rate changes).
|
|
129
|
+
*
|
|
130
|
+
* @param video - HTML video element
|
|
131
|
+
* @returns true if source is a MediaStream
|
|
132
|
+
*/
|
|
133
|
+
export function isMediaStreamSource(video: HTMLVideoElement | null): boolean {
|
|
134
|
+
if (!video) return false;
|
|
135
|
+
return video.srcObject instanceof MediaStream;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if playback rate adjustment is supported.
|
|
140
|
+
* WebRTC/MediaStream sources don't support playback rate changes.
|
|
141
|
+
*
|
|
142
|
+
* @param video - HTML video element
|
|
143
|
+
* @returns true if playback rate can be changed
|
|
144
|
+
*/
|
|
145
|
+
export function supportsPlaybackRate(video: HTMLVideoElement | null): boolean {
|
|
146
|
+
if (!video) return true;
|
|
147
|
+
return !isMediaStreamSource(video);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate seekable range for live or VOD streams.
|
|
152
|
+
*
|
|
153
|
+
* Priority order:
|
|
154
|
+
* 1. Browser's video.seekable ranges (most accurate for MSE-based players)
|
|
155
|
+
* 2. Track firstms/lastms from MistServer metadata
|
|
156
|
+
* 3. buffer_window from MistServer signaling
|
|
157
|
+
* 4. No fallback (treat as live-only when no reliable data)
|
|
158
|
+
*
|
|
159
|
+
* @param params - Calculation parameters
|
|
160
|
+
* @returns Seekable range with start and live edge
|
|
161
|
+
*/
|
|
162
|
+
export function calculateSeekableRange(params: SeekableRangeParams): SeekableRange {
|
|
163
|
+
const { isLive, video, mistStreamInfo, currentTime, duration, allowMediaStreamDvr = false } = params;
|
|
164
|
+
|
|
165
|
+
// VOD: full duration is seekable
|
|
166
|
+
if (!isLive) {
|
|
167
|
+
return { seekableStart: 0, liveEdge: duration };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isMediaStream = isMediaStreamSource(video);
|
|
171
|
+
|
|
172
|
+
// PRIORITY 1: Browser's video.seekable (most reliable - reflects actual browser state)
|
|
173
|
+
if (video?.seekable && video.seekable.length > 0) {
|
|
174
|
+
const start = video.seekable.start(0);
|
|
175
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
176
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
|
|
177
|
+
return { seekableStart: start, liveEdge: end };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// PRIORITY 2: Track firstms/lastms from MistServer (accurate when available)
|
|
182
|
+
// Skip for MediaStream unless explicitly allowed (e.g., WebCodecs DVR via server)
|
|
183
|
+
if ((allowMediaStreamDvr || !isMediaStream) && mistStreamInfo?.meta?.tracks) {
|
|
184
|
+
const tracks = Object.values(mistStreamInfo.meta.tracks) as MistTrackInfo[];
|
|
185
|
+
if (tracks.length > 0) {
|
|
186
|
+
const firstmsValues = tracks.map(t => t.firstms).filter((v): v is number => v !== undefined);
|
|
187
|
+
const lastmsValues = tracks.map(t => t.lastms).filter((v): v is number => v !== undefined);
|
|
188
|
+
|
|
189
|
+
if (firstmsValues.length > 0 && lastmsValues.length > 0) {
|
|
190
|
+
const firstms = Math.max(...firstmsValues);
|
|
191
|
+
const lastms = Math.min(...lastmsValues);
|
|
192
|
+
return { seekableStart: firstms / 1000, liveEdge: lastms / 1000 };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// PRIORITY 3: buffer_window from MistServer signaling
|
|
198
|
+
const bufferWindowMs = mistStreamInfo?.meta?.buffer_window;
|
|
199
|
+
if (bufferWindowMs && bufferWindowMs > 0 && currentTime > 0) {
|
|
200
|
+
const bufferWindowSec = bufferWindowMs / 1000;
|
|
201
|
+
return {
|
|
202
|
+
seekableStart: Math.max(0, currentTime - bufferWindowSec),
|
|
203
|
+
liveEdge: currentTime,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// No seekable range (live only)
|
|
208
|
+
return { seekableStart: currentTime, liveEdge: currentTime };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Determine if seeking is supported for the current stream.
|
|
213
|
+
*
|
|
214
|
+
* @param params - Check parameters
|
|
215
|
+
* @returns true if seeking is available
|
|
216
|
+
*/
|
|
217
|
+
export function canSeekStream(params: CanSeekParams): boolean {
|
|
218
|
+
const { video, isLive, duration, bufferWindowMs, playerCanSeek } = params;
|
|
219
|
+
|
|
220
|
+
// Player API says no
|
|
221
|
+
if (playerCanSeek && !playerCanSeek()) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Player API says yes - trust it for VOD, but require buffer for live
|
|
226
|
+
if (playerCanSeek && playerCanSeek()) {
|
|
227
|
+
if (!isLive) return true;
|
|
228
|
+
return bufferWindowMs !== undefined && bufferWindowMs > 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// No video element
|
|
232
|
+
if (!video) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// WebRTC/MediaStream: only if buffer_window explicitly configured
|
|
237
|
+
if (isMediaStreamSource(video)) {
|
|
238
|
+
return bufferWindowMs !== undefined && bufferWindowMs > 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Browser reports seekable ranges
|
|
242
|
+
if (video.seekable && video.seekable.length > 0) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// VOD with valid duration
|
|
247
|
+
if (!isLive && Number.isFinite(duration) && duration > 0) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Live with buffer_window configured
|
|
252
|
+
if (isLive && bufferWindowMs !== undefined && bufferWindowMs > 0) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Calculate live detection thresholds, optionally scaled by buffer_window.
|
|
261
|
+
*
|
|
262
|
+
* For medium/high latency tiers, scales thresholds based on the actual
|
|
263
|
+
* buffer window to provide more appropriate "near live" detection.
|
|
264
|
+
*
|
|
265
|
+
* @param sourceType - Protocol/MIME type for tier detection
|
|
266
|
+
* @param isWebRTC - Whether source is WebRTC (overrides tier to ultra-low)
|
|
267
|
+
* @param bufferWindowMs - Optional buffer window in milliseconds
|
|
268
|
+
* @returns Thresholds for entering/exiting "LIVE" state
|
|
269
|
+
*/
|
|
270
|
+
export function calculateLiveThresholds(
|
|
271
|
+
sourceType?: string,
|
|
272
|
+
isWebRTC?: boolean,
|
|
273
|
+
bufferWindowMs?: number
|
|
274
|
+
): LiveThresholds {
|
|
275
|
+
// Determine tier from source type, or use ultra-low for WebRTC
|
|
276
|
+
const tier = sourceType ? getLatencyTier(sourceType) : (isWebRTC ? 'ultra-low' : 'medium');
|
|
277
|
+
const tierThresholds = LATENCY_TIERS[tier];
|
|
278
|
+
|
|
279
|
+
// For medium/high tiers, scale thresholds based on buffer_window
|
|
280
|
+
if ((tier === 'medium' || tier === 'high') && bufferWindowMs && bufferWindowMs > 0) {
|
|
281
|
+
const bufferWindowSec = bufferWindowMs / 1000;
|
|
282
|
+
// Scale thresholds proportionally to buffer, with reasonable bounds
|
|
283
|
+
return {
|
|
284
|
+
exitLive: Math.max(tierThresholds.exitLive, Math.min(30, bufferWindowSec / 3)),
|
|
285
|
+
enterLive: Math.max(tierThresholds.enterLive, Math.min(10, bufferWindowSec / 10)),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return tierThresholds;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Calculate whether playback is "near live" using hysteresis.
|
|
294
|
+
*
|
|
295
|
+
* Hysteresis prevents flip-flopping when hovering near the threshold:
|
|
296
|
+
* - To EXIT "LIVE" state: must be > exitLive + margin behind
|
|
297
|
+
* - To ENTER "LIVE" state: must be < enterLive - margin behind
|
|
298
|
+
*
|
|
299
|
+
* @param currentTime - Current playback position in seconds
|
|
300
|
+
* @param liveEdge - Live edge position in seconds
|
|
301
|
+
* @param thresholds - Enter/exit thresholds
|
|
302
|
+
* @param currentState - Current isNearLive state
|
|
303
|
+
* @returns New isNearLive state
|
|
304
|
+
*/
|
|
305
|
+
export function calculateIsNearLive(
|
|
306
|
+
currentTime: number,
|
|
307
|
+
liveEdge: number,
|
|
308
|
+
thresholds: LiveThresholds,
|
|
309
|
+
currentState: boolean
|
|
310
|
+
): boolean {
|
|
311
|
+
// Invalid state - assume live
|
|
312
|
+
if (!Number.isFinite(liveEdge) || liveEdge <= 0) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const behindSeconds = liveEdge - currentTime;
|
|
317
|
+
|
|
318
|
+
// Hysteresis margins for extra stability
|
|
319
|
+
const exitMargin = 0.5;
|
|
320
|
+
const enterMargin = 0.2;
|
|
321
|
+
|
|
322
|
+
if (currentState && behindSeconds > thresholds.exitLive + exitMargin) {
|
|
323
|
+
// Currently "LIVE" - switch to "behind" when significantly behind
|
|
324
|
+
return false;
|
|
325
|
+
} else if (!currentState && behindSeconds < thresholds.enterLive - enterMargin) {
|
|
326
|
+
// Currently "behind" - switch to "LIVE" when close to live edge
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// No change
|
|
331
|
+
return currentState;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Determine if content is live based on available metadata.
|
|
336
|
+
*
|
|
337
|
+
* Priority:
|
|
338
|
+
* 1. Explicit isContentLive flag (highest priority)
|
|
339
|
+
* 2. MistServer stream type
|
|
340
|
+
* 3. Duration check (non-finite = live)
|
|
341
|
+
*
|
|
342
|
+
* @param isContentLive - Explicit live flag from content metadata
|
|
343
|
+
* @param mistStreamInfo - MistServer stream info
|
|
344
|
+
* @param duration - Video duration
|
|
345
|
+
* @returns true if content is live
|
|
346
|
+
*/
|
|
347
|
+
export function isLiveContent(
|
|
348
|
+
isContentLive?: boolean,
|
|
349
|
+
mistStreamInfo?: MistStreamInfo,
|
|
350
|
+
duration?: number
|
|
351
|
+
): boolean {
|
|
352
|
+
// Explicit flag wins
|
|
353
|
+
if (isContentLive !== undefined) {
|
|
354
|
+
return isContentLive;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// MistServer type
|
|
358
|
+
if (mistStreamInfo?.type) {
|
|
359
|
+
return mistStreamInfo.type === 'live';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Fallback: non-finite duration indicates live
|
|
363
|
+
return !Number.isFinite(duration);
|
|
364
|
+
}
|