@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.
Files changed (120) hide show
  1. package/dist/cjs/index.js +19493 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +19398 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/player.css +2140 -0
  6. package/dist/types/core/ABRController.d.ts +164 -0
  7. package/dist/types/core/CodecUtils.d.ts +54 -0
  8. package/dist/types/core/Disposable.d.ts +61 -0
  9. package/dist/types/core/EventEmitter.d.ts +73 -0
  10. package/dist/types/core/GatewayClient.d.ts +144 -0
  11. package/dist/types/core/InteractionController.d.ts +121 -0
  12. package/dist/types/core/LiveDurationProxy.d.ts +102 -0
  13. package/dist/types/core/MetaTrackManager.d.ts +220 -0
  14. package/dist/types/core/MistReporter.d.ts +163 -0
  15. package/dist/types/core/MistSignaling.d.ts +148 -0
  16. package/dist/types/core/PlayerController.d.ts +665 -0
  17. package/dist/types/core/PlayerInterface.d.ts +230 -0
  18. package/dist/types/core/PlayerManager.d.ts +182 -0
  19. package/dist/types/core/PlayerRegistry.d.ts +27 -0
  20. package/dist/types/core/QualityMonitor.d.ts +184 -0
  21. package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
  22. package/dist/types/core/SeekingUtils.d.ts +142 -0
  23. package/dist/types/core/StreamStateClient.d.ts +108 -0
  24. package/dist/types/core/SubtitleManager.d.ts +111 -0
  25. package/dist/types/core/TelemetryReporter.d.ts +79 -0
  26. package/dist/types/core/TimeFormat.d.ts +97 -0
  27. package/dist/types/core/TimerManager.d.ts +83 -0
  28. package/dist/types/core/UrlUtils.d.ts +81 -0
  29. package/dist/types/core/detector.d.ts +149 -0
  30. package/dist/types/core/index.d.ts +49 -0
  31. package/dist/types/core/scorer.d.ts +167 -0
  32. package/dist/types/core/selector.d.ts +9 -0
  33. package/dist/types/index.d.ts +45 -0
  34. package/dist/types/lib/utils.d.ts +2 -0
  35. package/dist/types/players/DashJsPlayer.d.ts +102 -0
  36. package/dist/types/players/HlsJsPlayer.d.ts +70 -0
  37. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
  38. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
  39. package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
  40. package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
  41. package/dist/types/players/MistPlayer.d.ts +25 -0
  42. package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
  43. package/dist/types/players/NativePlayer.d.ts +143 -0
  44. package/dist/types/players/VideoJsPlayer.d.ts +59 -0
  45. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
  46. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
  47. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
  48. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
  49. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
  50. package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
  51. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
  52. package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
  53. package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
  54. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
  55. package/dist/types/players/index.d.ts +14 -0
  56. package/dist/types/styles/index.d.ts +11 -0
  57. package/dist/types/types.d.ts +363 -0
  58. package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
  59. package/dist/types/vanilla/index.d.ts +19 -0
  60. package/dist/workers/decoder.worker.js +989 -0
  61. package/dist/workers/decoder.worker.js.map +1 -0
  62. package/package.json +80 -0
  63. package/src/core/ABRController.ts +550 -0
  64. package/src/core/CodecUtils.ts +257 -0
  65. package/src/core/Disposable.ts +120 -0
  66. package/src/core/EventEmitter.ts +113 -0
  67. package/src/core/GatewayClient.ts +439 -0
  68. package/src/core/InteractionController.ts +712 -0
  69. package/src/core/LiveDurationProxy.ts +270 -0
  70. package/src/core/MetaTrackManager.ts +753 -0
  71. package/src/core/MistReporter.ts +543 -0
  72. package/src/core/MistSignaling.ts +346 -0
  73. package/src/core/PlayerController.ts +2829 -0
  74. package/src/core/PlayerInterface.ts +432 -0
  75. package/src/core/PlayerManager.ts +900 -0
  76. package/src/core/PlayerRegistry.ts +149 -0
  77. package/src/core/QualityMonitor.ts +597 -0
  78. package/src/core/ScreenWakeLockManager.ts +163 -0
  79. package/src/core/SeekingUtils.ts +364 -0
  80. package/src/core/StreamStateClient.ts +457 -0
  81. package/src/core/SubtitleManager.ts +297 -0
  82. package/src/core/TelemetryReporter.ts +308 -0
  83. package/src/core/TimeFormat.ts +205 -0
  84. package/src/core/TimerManager.ts +209 -0
  85. package/src/core/UrlUtils.ts +179 -0
  86. package/src/core/detector.ts +382 -0
  87. package/src/core/index.ts +140 -0
  88. package/src/core/scorer.ts +553 -0
  89. package/src/core/selector.ts +16 -0
  90. package/src/global.d.ts +11 -0
  91. package/src/index.ts +75 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/players/DashJsPlayer.ts +642 -0
  94. package/src/players/HlsJsPlayer.ts +483 -0
  95. package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
  96. package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
  97. package/src/players/MewsWsPlayer/index.ts +1065 -0
  98. package/src/players/MewsWsPlayer/types.ts +106 -0
  99. package/src/players/MistPlayer.ts +188 -0
  100. package/src/players/MistWebRTCPlayer/index.ts +703 -0
  101. package/src/players/NativePlayer.ts +820 -0
  102. package/src/players/VideoJsPlayer.ts +643 -0
  103. package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
  104. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
  105. package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
  106. package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
  107. package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
  108. package/src/players/WebCodecsPlayer/index.ts +1650 -0
  109. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
  110. package/src/players/WebCodecsPlayer/types.ts +542 -0
  111. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
  112. package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
  113. package/src/players/index.ts +22 -0
  114. package/src/styles/animations.css +21 -0
  115. package/src/styles/index.ts +52 -0
  116. package/src/styles/player.css +2126 -0
  117. package/src/styles/tailwind.css +1015 -0
  118. package/src/types.ts +421 -0
  119. package/src/vanilla/FrameWorksPlayer.ts +367 -0
  120. 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
+ }