@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,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;
|