@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,457 @@
1
+ /**
2
+ * StreamStateClient.ts
3
+ *
4
+ * Framework-agnostic client for polling MistServer stream status via WebSocket or HTTP.
5
+ * Extracted from useStreamState.ts for use in headless core.
6
+ */
7
+
8
+ import { TypedEventEmitter } from './EventEmitter';
9
+ import { TimerManager } from './TimerManager';
10
+ import type { StreamState, StreamStatus, MistStreamInfo } from '../types';
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface StreamStateClientConfig {
17
+ /** MistServer base URL (e.g., https://mist.example.com) */
18
+ mistBaseUrl: string;
19
+ /** Stream name to poll */
20
+ streamName: string;
21
+ /** Poll interval in ms for HTTP fallback (default: 3000) */
22
+ pollInterval?: number;
23
+ /** Use WebSocket if available (default: true) */
24
+ useWebSocket?: boolean;
25
+ }
26
+
27
+ type StreamStateClientResolvedConfig = Omit<
28
+ StreamStateClientConfig,
29
+ 'pollInterval' | 'useWebSocket'
30
+ > & {
31
+ pollInterval: number;
32
+ useWebSocket: boolean;
33
+ };
34
+
35
+ export interface StreamStateClientEvents {
36
+ /** Emitted when stream state changes */
37
+ stateChange: { state: StreamState };
38
+ /** Emitted when stream comes online */
39
+ online: void;
40
+ /** Emitted when stream goes offline */
41
+ offline: void;
42
+ /** Emitted on connection error */
43
+ error: { error: string };
44
+ }
45
+
46
+ // ============================================================================
47
+ // Constants
48
+ // ============================================================================
49
+
50
+ const DEFAULT_POLL_INTERVAL = 3000;
51
+
52
+ const initialState: StreamState = {
53
+ status: 'OFFLINE',
54
+ isOnline: false,
55
+ message: 'Connecting...',
56
+ lastUpdate: 0,
57
+ };
58
+
59
+ // ============================================================================
60
+ // Helper Functions
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Parse MistServer error string into StreamStatus enum.
65
+ */
66
+ function parseErrorToStatus(error: string): StreamStatus {
67
+ const lowerError = error.toLowerCase();
68
+
69
+ if (lowerError.includes('offline')) return 'OFFLINE';
70
+ if (lowerError.includes('initializing')) return 'INITIALIZING';
71
+ if (lowerError.includes('booting')) return 'BOOTING';
72
+ if (lowerError.includes('waiting for data')) return 'WAITING_FOR_DATA';
73
+ if (lowerError.includes('shutting down')) return 'SHUTTING_DOWN';
74
+ if (lowerError.includes('invalid')) return 'INVALID';
75
+
76
+ return 'ERROR';
77
+ }
78
+
79
+ /**
80
+ * Get human-readable message for stream status.
81
+ */
82
+ function getStatusMessage(status: StreamStatus, percentage?: number): string {
83
+ switch (status) {
84
+ case 'ONLINE':
85
+ return 'Stream is online';
86
+ case 'OFFLINE':
87
+ return 'Stream is offline';
88
+ case 'INITIALIZING':
89
+ return percentage !== undefined
90
+ ? `Initializing... ${Math.round(percentage * 10) / 10}%`
91
+ : 'Stream is initializing';
92
+ case 'BOOTING':
93
+ return 'Stream is starting up';
94
+ case 'WAITING_FOR_DATA':
95
+ return 'Waiting for stream data';
96
+ case 'SHUTTING_DOWN':
97
+ return 'Stream is shutting down';
98
+ case 'INVALID':
99
+ return 'Stream status is invalid';
100
+ case 'ERROR':
101
+ default:
102
+ return 'Stream error';
103
+ }
104
+ }
105
+
106
+ // ============================================================================
107
+ // StreamStateClient Class
108
+ // ============================================================================
109
+
110
+ /**
111
+ * Client for polling MistServer stream status via WebSocket or HTTP.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const client = new StreamStateClient({
116
+ * mistBaseUrl: 'https://mist.example.com',
117
+ * streamName: 'my-stream',
118
+ * });
119
+ *
120
+ * client.on('stateChange', ({ state }) => console.log('State:', state));
121
+ * client.on('online', () => console.log('Stream is online!'));
122
+ * client.on('offline', () => console.log('Stream is offline'));
123
+ *
124
+ * client.start();
125
+ * // ...later
126
+ * client.stop();
127
+ * ```
128
+ */
129
+ export class StreamStateClient extends TypedEventEmitter<StreamStateClientEvents> {
130
+ private config: StreamStateClientResolvedConfig;
131
+ private state: StreamState = { ...initialState };
132
+ private ws: WebSocket | null = null;
133
+ private timers = new TimerManager();
134
+ private isRunning: boolean = false;
135
+ private wasOnline: boolean = false;
136
+ private connectionId: number = 0; // Track connection attempts to prevent stale callbacks
137
+
138
+ // Debounce time for rapid mount/unmount cycles (ms)
139
+ private static readonly CONNECTION_DEBOUNCE_MS = 100;
140
+
141
+ constructor(config: StreamStateClientConfig) {
142
+ super();
143
+ this.config = {
144
+ pollInterval: DEFAULT_POLL_INTERVAL,
145
+ useWebSocket: true,
146
+ ...config,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Start polling/WebSocket connection.
152
+ * Always does initial HTTP poll to get full stream info (including sources),
153
+ * then connects WebSocket for real-time status updates.
154
+ *
155
+ * Debounced to prevent orphaned connections during rapid mount/unmount cycles.
156
+ */
157
+ start(): void {
158
+ if (this.isRunning) return;
159
+ this.isRunning = true;
160
+
161
+ const { mistBaseUrl, streamName, useWebSocket } = this.config;
162
+
163
+ if (!mistBaseUrl || !streamName) {
164
+ console.warn('[StreamStateClient] Missing mistBaseUrl or streamName');
165
+ return;
166
+ }
167
+
168
+ // Reset state
169
+ this.setState({
170
+ ...initialState,
171
+ message: 'Connecting...',
172
+ lastUpdate: Date.now(),
173
+ });
174
+
175
+ // Increment connection ID to invalidate any pending callbacks from previous attempts
176
+ const currentConnectionId = ++this.connectionId;
177
+
178
+ // Debounce connection to prevent rapid reconnects during mount/unmount cycles
179
+ this.timers.start(() => {
180
+ // Check if this connection attempt is still valid
181
+ if (!this.isRunning || this.connectionId !== currentConnectionId) {
182
+ return;
183
+ }
184
+
185
+ // Always do initial HTTP poll to get full data (including sources)
186
+ // Then connect WebSocket for real-time updates
187
+ this.pollHttp().then(() => {
188
+ // Verify still valid before WebSocket connection
189
+ if (useWebSocket && this.isRunning && this.connectionId === currentConnectionId) {
190
+ this.connectWebSocket();
191
+ }
192
+ });
193
+ }, StreamStateClient.CONNECTION_DEBOUNCE_MS, 'connect');
194
+ }
195
+
196
+ /**
197
+ * Stop polling and close connections.
198
+ */
199
+ stop(): void {
200
+ this.isRunning = false;
201
+
202
+ // Close WebSocket
203
+ if (this.ws) {
204
+ this.ws.close();
205
+ this.ws = null;
206
+ }
207
+
208
+ // Clear all timers
209
+ this.timers.destroy();
210
+ }
211
+
212
+ /**
213
+ * Manual refresh - trigger an immediate poll.
214
+ */
215
+ refresh(): void {
216
+ if (this.config.useWebSocket && this.ws?.readyState === WebSocket.OPEN) {
217
+ // WebSocket will receive updates automatically
218
+ return;
219
+ }
220
+ this.pollHttp();
221
+ }
222
+
223
+ /**
224
+ * Get the underlying WebSocket connection (for MistReporter integration).
225
+ * Returns null if WebSocket is not connected.
226
+ */
227
+ getSocket(): WebSocket | null {
228
+ return this.ws;
229
+ }
230
+
231
+ /**
232
+ * Check if the WebSocket is connected and ready.
233
+ */
234
+ isSocketReady(): boolean {
235
+ return this.ws?.readyState === WebSocket.OPEN;
236
+ }
237
+
238
+ /**
239
+ * Get current stream state.
240
+ */
241
+ getState(): StreamState {
242
+ return { ...this.state };
243
+ }
244
+
245
+ /**
246
+ * Check if stream is online.
247
+ */
248
+ isOnline(): boolean {
249
+ return this.state.isOnline;
250
+ }
251
+
252
+ /**
253
+ * Update configuration (stops and restarts if running).
254
+ */
255
+ updateConfig(config: Partial<StreamStateClientConfig>): void {
256
+ const wasRunning = this.isRunning;
257
+ this.stop();
258
+ this.config = { ...this.config, ...config };
259
+ if (wasRunning) {
260
+ this.start();
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Clean up resources.
266
+ */
267
+ destroy(): void {
268
+ this.stop();
269
+ this.removeAllListeners();
270
+ }
271
+
272
+ // ============================================================================
273
+ // Private Methods
274
+ // ============================================================================
275
+
276
+ private connectWebSocket(): void {
277
+ if (!this.isRunning) return;
278
+
279
+ const { mistBaseUrl, streamName } = this.config;
280
+
281
+ // Clean up existing connection
282
+ if (this.ws) {
283
+ this.ws.close();
284
+ this.ws = null;
285
+ }
286
+
287
+ try {
288
+ // Convert http(s) to ws(s)
289
+ const wsUrl = mistBaseUrl
290
+ .replace(/^http:/, 'ws:')
291
+ .replace(/^https:/, 'wss:')
292
+ .replace(/\/$/, '');
293
+
294
+ const ws = new WebSocket(`${wsUrl}/json_${encodeURIComponent(streamName)}.js`);
295
+ this.ws = ws;
296
+
297
+ ws.onopen = () => {
298
+ console.debug('[StreamStateClient] WebSocket connected');
299
+ };
300
+
301
+ ws.onmessage = (event) => {
302
+ try {
303
+ const data = JSON.parse(event.data) as MistStreamInfo;
304
+ this.processStreamInfo(data);
305
+ } catch (e) {
306
+ console.warn('[StreamStateClient] Failed to parse WebSocket message:', e);
307
+ }
308
+ };
309
+
310
+ ws.onerror = () => {
311
+ console.warn('[StreamStateClient] WebSocket error, falling back to HTTP polling');
312
+ ws.close();
313
+ };
314
+
315
+ ws.onclose = () => {
316
+ this.ws = null;
317
+
318
+ if (!this.isRunning) return;
319
+
320
+ // Disable WebSocket and switch to HTTP polling
321
+ // This ensures pollHttp() schedules repeat polls (see line 365 condition)
322
+ this.config.useWebSocket = false;
323
+ console.debug('[StreamStateClient] WebSocket closed, switching to HTTP polling');
324
+ this.pollHttp();
325
+ };
326
+ } catch (error) {
327
+ console.warn('[StreamStateClient] WebSocket connection failed:', error);
328
+ // Disable WebSocket and switch to HTTP polling
329
+ this.config.useWebSocket = false;
330
+ this.pollHttp();
331
+ }
332
+ }
333
+
334
+ private async pollHttp(): Promise<void> {
335
+ if (!this.isRunning) return;
336
+
337
+ const { mistBaseUrl, streamName, pollInterval } = this.config;
338
+
339
+ try {
340
+ const url = `${mistBaseUrl.replace(/\/$/, '')}/json_${encodeURIComponent(streamName)}.js`;
341
+ const response = await fetch(url, {
342
+ method: 'GET',
343
+ headers: { 'Accept': 'application/json' },
344
+ });
345
+
346
+ if (!response.ok) {
347
+ throw new Error(`HTTP ${response.status}`);
348
+ }
349
+
350
+ // MistServer returns JSON with potential JSONP wrapper
351
+ let text = await response.text();
352
+ // Strip JSONP callback if present
353
+ const jsonpMatch = text.match(/^[^(]+\(([\s\S]*)\);?$/);
354
+ if (jsonpMatch) {
355
+ text = jsonpMatch[1];
356
+ }
357
+
358
+ const data = JSON.parse(text) as MistStreamInfo;
359
+ this.processStreamInfo(data);
360
+ } catch (error) {
361
+ if (!this.isRunning) return;
362
+
363
+ const errorMessage = error instanceof Error ? error.message : 'Connection failed';
364
+ this.setState({
365
+ ...this.state,
366
+ status: 'ERROR',
367
+ isOnline: false,
368
+ message: errorMessage,
369
+ lastUpdate: Date.now(),
370
+ error: errorMessage,
371
+ });
372
+ this.emit('error', { error: errorMessage });
373
+ }
374
+
375
+ // Schedule next poll
376
+ if (this.isRunning && !this.config.useWebSocket) {
377
+ this.timers.start(() => this.pollHttp(), pollInterval, 'poll');
378
+ }
379
+ }
380
+
381
+ private processStreamInfo(data: MistStreamInfo): void {
382
+ if (!this.isRunning) return;
383
+
384
+ let newState: StreamState;
385
+
386
+ if (data.error) {
387
+ // Stream has an error state - preserve existing streamInfo
388
+ const status = parseErrorToStatus(data.error);
389
+ const message = data.on_error || getStatusMessage(status, data.perc);
390
+
391
+ newState = {
392
+ status,
393
+ isOnline: false,
394
+ message,
395
+ percentage: data.perc,
396
+ lastUpdate: Date.now(),
397
+ error: data.error,
398
+ streamInfo: this.state.streamInfo, // Preserve existing source/tracks
399
+ };
400
+ } else {
401
+ // Stream is online with valid metadata
402
+ // Merge new data with existing streamInfo to preserve source/tracks from initial fetch
403
+ // WebSocket updates may not include source array
404
+ const mergedStreamInfo: MistStreamInfo = {
405
+ ...this.state.streamInfo, // Keep existing source/meta if present
406
+ ...data, // Override with new data
407
+ // Explicitly preserve source if not in new data
408
+ source: data.source || this.state.streamInfo?.source,
409
+ // Merge meta to preserve tracks
410
+ meta: {
411
+ ...this.state.streamInfo?.meta,
412
+ ...data.meta,
413
+ // Preserve tracks if not in new data
414
+ tracks: data.meta?.tracks || this.state.streamInfo?.meta?.tracks,
415
+ },
416
+ };
417
+
418
+ newState = {
419
+ status: 'ONLINE',
420
+ isOnline: true,
421
+ message: 'Stream is online',
422
+ lastUpdate: Date.now(),
423
+ streamInfo: mergedStreamInfo,
424
+ };
425
+ }
426
+
427
+ this.setState(newState);
428
+
429
+ // Emit online/offline events on state transitions
430
+ if (newState.isOnline && !this.wasOnline) {
431
+ this.emit('online', undefined as never);
432
+ } else if (!newState.isOnline && this.wasOnline) {
433
+ this.emit('offline', undefined as never);
434
+ }
435
+ this.wasOnline = newState.isOnline;
436
+ }
437
+
438
+ private setState(state: StreamState): void {
439
+ const prevState = this.state;
440
+ this.state = state;
441
+
442
+ // Emit if ANY state field changed - including streamInfo (track data)
443
+ // Previously only checked status/isOnline/message, causing track updates to be lost
444
+ const hasChanged =
445
+ prevState.status !== state.status ||
446
+ prevState.isOnline !== state.isOnline ||
447
+ prevState.message !== state.message ||
448
+ prevState.streamInfo !== state.streamInfo ||
449
+ prevState.lastUpdate !== state.lastUpdate;
450
+
451
+ if (hasChanged) {
452
+ this.emit('stateChange', { state });
453
+ }
454
+ }
455
+ }
456
+
457
+ export default StreamStateClient;