@livepeer-frameworks/player-core 0.1.1 → 0.1.2
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/core/ABRController.js +456 -0
- package/dist/cjs/core/ABRController.js.map +1 -0
- package/dist/cjs/core/CodecUtils.js +195 -0
- package/dist/cjs/core/CodecUtils.js.map +1 -0
- package/dist/cjs/core/ErrorClassifier.js +410 -0
- package/dist/cjs/core/ErrorClassifier.js.map +1 -0
- package/dist/cjs/core/EventEmitter.js +108 -0
- package/dist/cjs/core/EventEmitter.js.map +1 -0
- package/dist/cjs/core/GatewayClient.js +342 -0
- package/dist/cjs/core/GatewayClient.js.map +1 -0
- package/dist/cjs/core/InteractionController.js +606 -0
- package/dist/cjs/core/InteractionController.js.map +1 -0
- package/dist/cjs/core/LiveDurationProxy.js +186 -0
- package/dist/cjs/core/LiveDurationProxy.js.map +1 -0
- package/dist/cjs/core/MetaTrackManager.js +624 -0
- package/dist/cjs/core/MetaTrackManager.js.map +1 -0
- package/dist/cjs/core/MistReporter.js +449 -0
- package/dist/cjs/core/MistReporter.js.map +1 -0
- package/dist/cjs/core/MistSignaling.js +264 -0
- package/dist/cjs/core/MistSignaling.js.map +1 -0
- package/dist/cjs/core/PlayerController.js +2658 -0
- package/dist/cjs/core/PlayerController.js.map +1 -0
- package/dist/cjs/core/PlayerInterface.js +269 -0
- package/dist/cjs/core/PlayerInterface.js.map +1 -0
- package/dist/cjs/core/PlayerManager.js +806 -0
- package/dist/cjs/core/PlayerManager.js.map +1 -0
- package/dist/cjs/core/PlayerRegistry.js +270 -0
- package/dist/cjs/core/PlayerRegistry.js.map +1 -0
- package/dist/cjs/core/QualityMonitor.js +474 -0
- package/dist/cjs/core/QualityMonitor.js.map +1 -0
- package/dist/cjs/core/SeekingUtils.js +292 -0
- package/dist/cjs/core/SeekingUtils.js.map +1 -0
- package/dist/cjs/core/StreamStateClient.js +381 -0
- package/dist/cjs/core/StreamStateClient.js.map +1 -0
- package/dist/cjs/core/SubtitleManager.js +227 -0
- package/dist/cjs/core/SubtitleManager.js.map +1 -0
- package/dist/cjs/core/TelemetryReporter.js +258 -0
- package/dist/cjs/core/TelemetryReporter.js.map +1 -0
- package/dist/cjs/core/TimeFormat.js +176 -0
- package/dist/cjs/core/TimeFormat.js.map +1 -0
- package/dist/cjs/core/TimerManager.js +176 -0
- package/dist/cjs/core/TimerManager.js.map +1 -0
- package/dist/cjs/core/UrlUtils.js +160 -0
- package/dist/cjs/core/UrlUtils.js.map +1 -0
- package/dist/cjs/core/detector.js +293 -0
- package/dist/cjs/core/detector.js.map +1 -0
- package/dist/cjs/core/scorer.js +443 -0
- package/dist/cjs/core/scorer.js.map +1 -0
- package/dist/cjs/index.js +121 -20134
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/utils.js +11 -0
- package/dist/cjs/lib/utils.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +6 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3042 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/cjs/players/DashJsPlayer.js +638 -0
- package/dist/cjs/players/DashJsPlayer.js.map +1 -0
- package/dist/cjs/players/HlsJsPlayer.js +482 -0
- package/dist/cjs/players/HlsJsPlayer.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js +522 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js +215 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/index.js +987 -0
- package/dist/cjs/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/cjs/players/MistPlayer.js +185 -0
- package/dist/cjs/players/MistPlayer.js.map +1 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js +635 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/cjs/players/NativePlayer.js +762 -0
- package/dist/cjs/players/NativePlayer.js.map +1 -0
- package/dist/cjs/players/VideoJsPlayer.js +585 -0
- package/dist/cjs/players/VideoJsPlayer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js +236 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js +143 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js +96 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js +359 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js +460 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js +1467 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +320 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/cjs/styles/index.js +57 -0
- package/dist/cjs/styles/index.js.map +1 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js +269 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/cjs/vanilla.js +11 -0
- package/dist/cjs/vanilla.js.map +1 -0
- package/dist/esm/core/ABRController.js +454 -0
- package/dist/esm/core/ABRController.js.map +1 -0
- package/dist/esm/core/CodecUtils.js +193 -0
- package/dist/esm/core/CodecUtils.js.map +1 -0
- package/dist/esm/core/ErrorClassifier.js +408 -0
- package/dist/esm/core/ErrorClassifier.js.map +1 -0
- package/dist/esm/core/EventEmitter.js +106 -0
- package/dist/esm/core/EventEmitter.js.map +1 -0
- package/dist/esm/core/GatewayClient.js +340 -0
- package/dist/esm/core/GatewayClient.js.map +1 -0
- package/dist/esm/core/InteractionController.js +604 -0
- package/dist/esm/core/InteractionController.js.map +1 -0
- package/dist/esm/core/LiveDurationProxy.js +184 -0
- package/dist/esm/core/LiveDurationProxy.js.map +1 -0
- package/dist/esm/core/MetaTrackManager.js +622 -0
- package/dist/esm/core/MetaTrackManager.js.map +1 -0
- package/dist/esm/core/MistReporter.js +447 -0
- package/dist/esm/core/MistReporter.js.map +1 -0
- package/dist/esm/core/MistSignaling.js +262 -0
- package/dist/esm/core/MistSignaling.js.map +1 -0
- package/dist/esm/core/PlayerController.js +2651 -0
- package/dist/esm/core/PlayerController.js.map +1 -0
- package/dist/esm/core/PlayerInterface.js +267 -0
- package/dist/esm/core/PlayerInterface.js.map +1 -0
- package/dist/esm/core/PlayerManager.js +804 -0
- package/dist/esm/core/PlayerManager.js.map +1 -0
- package/dist/esm/core/PlayerRegistry.js +264 -0
- package/dist/esm/core/PlayerRegistry.js.map +1 -0
- package/dist/esm/core/QualityMonitor.js +471 -0
- package/dist/esm/core/QualityMonitor.js.map +1 -0
- package/dist/esm/core/SeekingUtils.js +280 -0
- package/dist/esm/core/SeekingUtils.js.map +1 -0
- package/dist/esm/core/StreamStateClient.js +379 -0
- package/dist/esm/core/StreamStateClient.js.map +1 -0
- package/dist/esm/core/SubtitleManager.js +225 -0
- package/dist/esm/core/SubtitleManager.js.map +1 -0
- package/dist/esm/core/TelemetryReporter.js +256 -0
- package/dist/esm/core/TelemetryReporter.js.map +1 -0
- package/dist/esm/core/TimeFormat.js +169 -0
- package/dist/esm/core/TimeFormat.js.map +1 -0
- package/dist/esm/core/TimerManager.js +174 -0
- package/dist/esm/core/TimerManager.js.map +1 -0
- package/dist/esm/core/UrlUtils.js +151 -0
- package/dist/esm/core/UrlUtils.js.map +1 -0
- package/dist/esm/core/detector.js +279 -0
- package/dist/esm/core/detector.js.map +1 -0
- package/dist/esm/core/scorer.js +422 -0
- package/dist/esm/core/scorer.js.map +1 -0
- package/dist/esm/index.js +26 -20043
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/utils.js +9 -0
- package/dist/esm/lib/utils.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +4 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3036 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/esm/players/DashJsPlayer.js +636 -0
- package/dist/esm/players/DashJsPlayer.js.map +1 -0
- package/dist/esm/players/HlsJsPlayer.js +480 -0
- package/dist/esm/players/HlsJsPlayer.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js +520 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js +213 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/index.js +985 -0
- package/dist/esm/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/esm/players/MistPlayer.js +183 -0
- package/dist/esm/players/MistPlayer.js.map +1 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js +633 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/esm/players/NativePlayer.js +759 -0
- package/dist/esm/players/NativePlayer.js.map +1 -0
- package/dist/esm/players/VideoJsPlayer.js +583 -0
- package/dist/esm/players/VideoJsPlayer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js +233 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js +134 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js +91 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js +357 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js +458 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/index.js +1458 -0
- package/dist/esm/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +315 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/esm/styles/index.js +54 -0
- package/dist/esm/styles/index.js.map +1 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js +264 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/esm/vanilla.js +2 -0
- package/dist/esm/vanilla.js.map +1 -0
- package/dist/player.css +4 -1
- package/dist/types/core/ABRController.d.ts +4 -4
- package/dist/types/core/CodecUtils.d.ts +1 -1
- package/dist/types/core/ErrorClassifier.d.ts +77 -0
- package/dist/types/core/GatewayClient.d.ts +4 -4
- package/dist/types/core/MetaTrackManager.d.ts +2 -2
- package/dist/types/core/MistReporter.d.ts +3 -3
- package/dist/types/core/MistSignaling.d.ts +12 -12
- package/dist/types/core/PlayerController.d.ts +19 -14
- package/dist/types/core/PlayerInterface.d.ts +100 -2
- package/dist/types/core/PlayerManager.d.ts +36 -9
- package/dist/types/core/PlayerRegistry.d.ts +11 -11
- package/dist/types/core/QualityMonitor.d.ts +2 -2
- package/dist/types/core/SeekingUtils.d.ts +2 -2
- package/dist/types/core/StreamStateClient.d.ts +2 -2
- package/dist/types/core/TelemetryReporter.d.ts +1 -1
- package/dist/types/core/TimerManager.d.ts +1 -1
- package/dist/types/core/detector.d.ts +1 -1
- package/dist/types/core/index.d.ts +44 -44
- package/dist/types/core/scorer.d.ts +1 -1
- package/dist/types/core/selector.d.ts +2 -2
- package/dist/types/index.d.ts +35 -34
- package/dist/types/players/DashJsPlayer.d.ts +3 -3
- package/dist/types/players/HlsJsPlayer.d.ts +3 -3
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/index.d.ts +2 -2
- package/dist/types/players/MewsWsPlayer/types.d.ts +15 -15
- package/dist/types/players/MistPlayer.d.ts +2 -2
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +3 -3
- package/dist/types/players/NativePlayer.d.ts +3 -3
- package/dist/types/players/VideoJsPlayer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/index.d.ts +9 -9
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/types.d.ts +49 -49
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +31 -31
- package/dist/types/players/index.d.ts +5 -8
- package/dist/types/types.d.ts +15 -15
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +2 -2
- package/dist/types/vanilla/index.d.ts +4 -4
- package/dist/workers/decoder.worker.js +129 -122
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +31 -15
- package/src/core/ErrorClassifier.ts +499 -0
- package/src/core/PlayerController.ts +17 -2
- package/src/core/PlayerInterface.ts +109 -0
- package/src/core/PlayerManager.ts +290 -46
- package/src/core/PlayerRegistry.ts +221 -87
- package/src/core/TelemetryReporter.ts +4 -1
- package/src/index.ts +13 -4
- package/src/players/WebCodecsPlayer/index.ts +2 -2
- package/src/players/index.ts +5 -16
- package/src/styles/player.css +4 -1
- package/src/vanilla/FrameWorksPlayer.ts +2 -5
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { TypedEventEmitter } from './EventEmitter.js';
|
|
2
|
+
import { TimerManager } from './TimerManager.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* StreamStateClient.ts
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic client for polling MistServer stream status via WebSocket or HTTP.
|
|
8
|
+
* Extracted from useStreamState.ts for use in headless core.
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const DEFAULT_POLL_INTERVAL = 3000;
|
|
14
|
+
const initialState = {
|
|
15
|
+
status: "OFFLINE",
|
|
16
|
+
isOnline: false,
|
|
17
|
+
message: "Connecting...",
|
|
18
|
+
lastUpdate: 0,
|
|
19
|
+
};
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Helper Functions
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Parse MistServer error string into StreamStatus enum.
|
|
25
|
+
*/
|
|
26
|
+
function parseErrorToStatus(error) {
|
|
27
|
+
const lowerError = error.toLowerCase();
|
|
28
|
+
if (lowerError.includes("offline"))
|
|
29
|
+
return "OFFLINE";
|
|
30
|
+
if (lowerError.includes("initializing"))
|
|
31
|
+
return "INITIALIZING";
|
|
32
|
+
if (lowerError.includes("booting"))
|
|
33
|
+
return "BOOTING";
|
|
34
|
+
if (lowerError.includes("waiting for data"))
|
|
35
|
+
return "WAITING_FOR_DATA";
|
|
36
|
+
if (lowerError.includes("shutting down"))
|
|
37
|
+
return "SHUTTING_DOWN";
|
|
38
|
+
if (lowerError.includes("invalid"))
|
|
39
|
+
return "INVALID";
|
|
40
|
+
return "ERROR";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get human-readable message for stream status.
|
|
44
|
+
*/
|
|
45
|
+
function getStatusMessage(status, percentage) {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "ONLINE":
|
|
48
|
+
return "Stream is online";
|
|
49
|
+
case "OFFLINE":
|
|
50
|
+
return "Stream is offline";
|
|
51
|
+
case "INITIALIZING":
|
|
52
|
+
return percentage !== undefined
|
|
53
|
+
? `Initializing... ${Math.round(percentage * 10) / 10}%`
|
|
54
|
+
: "Stream is initializing";
|
|
55
|
+
case "BOOTING":
|
|
56
|
+
return "Stream is starting up";
|
|
57
|
+
case "WAITING_FOR_DATA":
|
|
58
|
+
return "Waiting for stream data";
|
|
59
|
+
case "SHUTTING_DOWN":
|
|
60
|
+
return "Stream is shutting down";
|
|
61
|
+
case "INVALID":
|
|
62
|
+
return "Stream status is invalid";
|
|
63
|
+
case "ERROR":
|
|
64
|
+
default:
|
|
65
|
+
return "Stream error";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// StreamStateClient Class
|
|
70
|
+
// ============================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Client for polling MistServer stream status via WebSocket or HTTP.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const client = new StreamStateClient({
|
|
77
|
+
* mistBaseUrl: 'https://mist.example.com',
|
|
78
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* client.on('stateChange', ({ state }) => console.log('State:', state));
|
|
82
|
+
* client.on('online', () => console.log('Stream is online!'));
|
|
83
|
+
* client.on('offline', () => console.log('Stream is offline'));
|
|
84
|
+
*
|
|
85
|
+
* client.start();
|
|
86
|
+
* // ...later
|
|
87
|
+
* client.stop();
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
class StreamStateClient extends TypedEventEmitter {
|
|
91
|
+
constructor(config) {
|
|
92
|
+
super();
|
|
93
|
+
this.state = { ...initialState };
|
|
94
|
+
this.ws = null;
|
|
95
|
+
this.timers = new TimerManager();
|
|
96
|
+
this.isRunning = false;
|
|
97
|
+
this.wasOnline = false;
|
|
98
|
+
this.connectionId = 0; // Track connection attempts to prevent stale callbacks
|
|
99
|
+
this.config = {
|
|
100
|
+
pollInterval: DEFAULT_POLL_INTERVAL,
|
|
101
|
+
useWebSocket: true,
|
|
102
|
+
...config,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Start polling/WebSocket connection.
|
|
107
|
+
* Always does initial HTTP poll to get full stream info (including sources),
|
|
108
|
+
* then connects WebSocket for real-time status updates.
|
|
109
|
+
*
|
|
110
|
+
* Debounced to prevent orphaned connections during rapid mount/unmount cycles.
|
|
111
|
+
*/
|
|
112
|
+
start() {
|
|
113
|
+
if (this.isRunning)
|
|
114
|
+
return;
|
|
115
|
+
this.isRunning = true;
|
|
116
|
+
const { mistBaseUrl, streamName, useWebSocket } = this.config;
|
|
117
|
+
if (!mistBaseUrl || !streamName) {
|
|
118
|
+
console.warn("[StreamStateClient] Missing mistBaseUrl or streamName");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Reset state
|
|
122
|
+
this.setState({
|
|
123
|
+
...initialState,
|
|
124
|
+
message: "Connecting...",
|
|
125
|
+
lastUpdate: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
// Increment connection ID to invalidate any pending callbacks from previous attempts
|
|
128
|
+
const currentConnectionId = ++this.connectionId;
|
|
129
|
+
// Debounce connection to prevent rapid reconnects during mount/unmount cycles
|
|
130
|
+
this.timers.start(() => {
|
|
131
|
+
// Check if this connection attempt is still valid
|
|
132
|
+
if (!this.isRunning || this.connectionId !== currentConnectionId) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Always do initial HTTP poll to get full data (including sources)
|
|
136
|
+
// Then connect WebSocket for real-time updates
|
|
137
|
+
this.pollHttp().then(() => {
|
|
138
|
+
// Verify still valid before WebSocket connection
|
|
139
|
+
if (useWebSocket && this.isRunning && this.connectionId === currentConnectionId) {
|
|
140
|
+
this.connectWebSocket();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}, StreamStateClient.CONNECTION_DEBOUNCE_MS, "connect");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Stop polling and close connections.
|
|
147
|
+
*/
|
|
148
|
+
stop() {
|
|
149
|
+
this.isRunning = false;
|
|
150
|
+
// Close WebSocket
|
|
151
|
+
if (this.ws) {
|
|
152
|
+
this.ws.close();
|
|
153
|
+
this.ws = null;
|
|
154
|
+
}
|
|
155
|
+
// Clear all timers
|
|
156
|
+
this.timers.destroy();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Manual refresh - trigger an immediate poll.
|
|
160
|
+
*/
|
|
161
|
+
refresh() {
|
|
162
|
+
if (this.config.useWebSocket && this.ws?.readyState === WebSocket.OPEN) {
|
|
163
|
+
// WebSocket will receive updates automatically
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.pollHttp();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the underlying WebSocket connection (for MistReporter integration).
|
|
170
|
+
* Returns null if WebSocket is not connected.
|
|
171
|
+
*/
|
|
172
|
+
getSocket() {
|
|
173
|
+
return this.ws;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if the WebSocket is connected and ready.
|
|
177
|
+
*/
|
|
178
|
+
isSocketReady() {
|
|
179
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get current stream state.
|
|
183
|
+
*/
|
|
184
|
+
getState() {
|
|
185
|
+
return { ...this.state };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if stream is online.
|
|
189
|
+
*/
|
|
190
|
+
isOnline() {
|
|
191
|
+
return this.state.isOnline;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Update configuration (stops and restarts if running).
|
|
195
|
+
*/
|
|
196
|
+
updateConfig(config) {
|
|
197
|
+
const wasRunning = this.isRunning;
|
|
198
|
+
this.stop();
|
|
199
|
+
this.config = { ...this.config, ...config };
|
|
200
|
+
if (wasRunning) {
|
|
201
|
+
this.start();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Clean up resources.
|
|
206
|
+
*/
|
|
207
|
+
destroy() {
|
|
208
|
+
this.stop();
|
|
209
|
+
this.removeAllListeners();
|
|
210
|
+
}
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Private Methods
|
|
213
|
+
// ============================================================================
|
|
214
|
+
connectWebSocket() {
|
|
215
|
+
if (!this.isRunning)
|
|
216
|
+
return;
|
|
217
|
+
const { mistBaseUrl, streamName } = this.config;
|
|
218
|
+
// Clean up existing connection
|
|
219
|
+
if (this.ws) {
|
|
220
|
+
this.ws.close();
|
|
221
|
+
this.ws = null;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
// Convert http(s) to ws(s)
|
|
225
|
+
const wsUrl = mistBaseUrl
|
|
226
|
+
.replace(/^http:/, "ws:")
|
|
227
|
+
.replace(/^https:/, "wss:")
|
|
228
|
+
.replace(/\/$/, "");
|
|
229
|
+
const ws = new WebSocket(`${wsUrl}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`);
|
|
230
|
+
this.ws = ws;
|
|
231
|
+
ws.onopen = () => {
|
|
232
|
+
console.debug("[StreamStateClient] WebSocket connected");
|
|
233
|
+
};
|
|
234
|
+
ws.onmessage = (event) => {
|
|
235
|
+
try {
|
|
236
|
+
const data = JSON.parse(event.data);
|
|
237
|
+
this.processStreamInfo(data);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
console.warn("[StreamStateClient] Failed to parse WebSocket message:", e);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
ws.onerror = () => {
|
|
244
|
+
console.warn("[StreamStateClient] WebSocket error, falling back to HTTP polling");
|
|
245
|
+
ws.close();
|
|
246
|
+
};
|
|
247
|
+
ws.onclose = () => {
|
|
248
|
+
this.ws = null;
|
|
249
|
+
if (!this.isRunning)
|
|
250
|
+
return;
|
|
251
|
+
// Disable WebSocket and switch to HTTP polling
|
|
252
|
+
// This ensures pollHttp() schedules repeat polls (see line 365 condition)
|
|
253
|
+
this.config.useWebSocket = false;
|
|
254
|
+
console.debug("[StreamStateClient] WebSocket closed, switching to HTTP polling");
|
|
255
|
+
this.pollHttp();
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.warn("[StreamStateClient] WebSocket connection failed:", error);
|
|
260
|
+
// Disable WebSocket and switch to HTTP polling
|
|
261
|
+
this.config.useWebSocket = false;
|
|
262
|
+
this.pollHttp();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async pollHttp() {
|
|
266
|
+
if (!this.isRunning)
|
|
267
|
+
return;
|
|
268
|
+
const { mistBaseUrl, streamName, pollInterval } = this.config;
|
|
269
|
+
try {
|
|
270
|
+
const url = `${mistBaseUrl.replace(/\/$/, "")}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`;
|
|
271
|
+
const response = await fetch(url, {
|
|
272
|
+
method: "GET",
|
|
273
|
+
headers: { Accept: "application/json" },
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(`HTTP ${response.status}`);
|
|
277
|
+
}
|
|
278
|
+
// MistServer returns JSON with potential JSONP wrapper
|
|
279
|
+
let text = await response.text();
|
|
280
|
+
// Strip JSONP callback if present
|
|
281
|
+
const jsonpMatch = text.match(/^[^(]+\(([\s\S]*)\);?$/);
|
|
282
|
+
if (jsonpMatch) {
|
|
283
|
+
text = jsonpMatch[1];
|
|
284
|
+
}
|
|
285
|
+
const data = JSON.parse(text);
|
|
286
|
+
this.processStreamInfo(data);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (!this.isRunning)
|
|
290
|
+
return;
|
|
291
|
+
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
292
|
+
this.setState({
|
|
293
|
+
...this.state,
|
|
294
|
+
status: "ERROR",
|
|
295
|
+
isOnline: false,
|
|
296
|
+
message: errorMessage,
|
|
297
|
+
lastUpdate: Date.now(),
|
|
298
|
+
error: errorMessage,
|
|
299
|
+
});
|
|
300
|
+
this.emit("error", { error: errorMessage });
|
|
301
|
+
}
|
|
302
|
+
// Schedule next poll
|
|
303
|
+
if (this.isRunning && !this.config.useWebSocket) {
|
|
304
|
+
this.timers.start(() => this.pollHttp(), pollInterval, "poll");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
processStreamInfo(data) {
|
|
308
|
+
if (!this.isRunning)
|
|
309
|
+
return;
|
|
310
|
+
let newState;
|
|
311
|
+
if (data.error) {
|
|
312
|
+
// Stream has an error state - preserve existing streamInfo
|
|
313
|
+
const status = parseErrorToStatus(data.error);
|
|
314
|
+
const message = data.on_error || getStatusMessage(status, data.perc);
|
|
315
|
+
newState = {
|
|
316
|
+
status,
|
|
317
|
+
isOnline: false,
|
|
318
|
+
message,
|
|
319
|
+
percentage: data.perc,
|
|
320
|
+
lastUpdate: Date.now(),
|
|
321
|
+
error: data.error,
|
|
322
|
+
streamInfo: this.state.streamInfo, // Preserve existing source/tracks
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// Stream is online with valid metadata
|
|
327
|
+
// Merge new data with existing streamInfo to preserve source/tracks from initial fetch
|
|
328
|
+
// WebSocket updates may not include source array
|
|
329
|
+
const mergedStreamInfo = {
|
|
330
|
+
...this.state.streamInfo, // Keep existing source/meta if present
|
|
331
|
+
...data, // Override with new data
|
|
332
|
+
// Explicitly preserve source if not in new data
|
|
333
|
+
source: data.source || this.state.streamInfo?.source,
|
|
334
|
+
// Merge meta to preserve tracks
|
|
335
|
+
meta: {
|
|
336
|
+
...this.state.streamInfo?.meta,
|
|
337
|
+
...data.meta,
|
|
338
|
+
// Preserve tracks if not in new data
|
|
339
|
+
tracks: data.meta?.tracks || this.state.streamInfo?.meta?.tracks,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
newState = {
|
|
343
|
+
status: "ONLINE",
|
|
344
|
+
isOnline: true,
|
|
345
|
+
message: "Stream is online",
|
|
346
|
+
lastUpdate: Date.now(),
|
|
347
|
+
streamInfo: mergedStreamInfo,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
this.setState(newState);
|
|
351
|
+
// Emit online/offline events on state transitions
|
|
352
|
+
if (newState.isOnline && !this.wasOnline) {
|
|
353
|
+
this.emit("online", undefined);
|
|
354
|
+
}
|
|
355
|
+
else if (!newState.isOnline && this.wasOnline) {
|
|
356
|
+
this.emit("offline", undefined);
|
|
357
|
+
}
|
|
358
|
+
this.wasOnline = newState.isOnline;
|
|
359
|
+
}
|
|
360
|
+
setState(state) {
|
|
361
|
+
const prevState = this.state;
|
|
362
|
+
this.state = state;
|
|
363
|
+
// Emit if ANY state field changed - including streamInfo (track data)
|
|
364
|
+
// Previously only checked status/isOnline/message, causing track updates to be lost
|
|
365
|
+
const hasChanged = prevState.status !== state.status ||
|
|
366
|
+
prevState.isOnline !== state.isOnline ||
|
|
367
|
+
prevState.message !== state.message ||
|
|
368
|
+
prevState.streamInfo !== state.streamInfo ||
|
|
369
|
+
prevState.lastUpdate !== state.lastUpdate;
|
|
370
|
+
if (hasChanged) {
|
|
371
|
+
this.emit("stateChange", { state });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Debounce time for rapid mount/unmount cycles (ms)
|
|
376
|
+
StreamStateClient.CONNECTION_DEBOUNCE_MS = 100;
|
|
377
|
+
|
|
378
|
+
export { StreamStateClient };
|
|
379
|
+
//# sourceMappingURL=StreamStateClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StreamStateClient.js","sources":["../../../../src/core/StreamStateClient.ts"],"sourcesContent":["/**\n * StreamStateClient.ts\n *\n * Framework-agnostic client for polling MistServer stream status via WebSocket or HTTP.\n * Extracted from useStreamState.ts for use in headless core.\n */\n\nimport { TypedEventEmitter } from \"./EventEmitter\";\nimport { TimerManager } from \"./TimerManager\";\nimport type { StreamState, StreamStatus, MistStreamInfo } from \"../types\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface StreamStateClientConfig {\n /** MistServer base URL (e.g., https://mist.example.com) */\n mistBaseUrl: string;\n /** Stream name to poll */\n streamName: string;\n /** Poll interval in ms for HTTP fallback (default: 3000) */\n pollInterval?: number;\n /** Use WebSocket if available (default: true) */\n useWebSocket?: boolean;\n}\n\ntype StreamStateClientResolvedConfig = Omit<\n StreamStateClientConfig,\n \"pollInterval\" | \"useWebSocket\"\n> & {\n pollInterval: number;\n useWebSocket: boolean;\n};\n\nexport interface StreamStateClientEvents {\n /** Emitted when stream state changes */\n stateChange: { state: StreamState };\n /** Emitted when stream comes online */\n online: void;\n /** Emitted when stream goes offline */\n offline: void;\n /** Emitted on connection error */\n error: { error: string };\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_POLL_INTERVAL = 3000;\n\nconst initialState: StreamState = {\n status: \"OFFLINE\",\n isOnline: false,\n message: \"Connecting...\",\n lastUpdate: 0,\n};\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Parse MistServer error string into StreamStatus enum.\n */\nfunction parseErrorToStatus(error: string): StreamStatus {\n const lowerError = error.toLowerCase();\n\n if (lowerError.includes(\"offline\")) return \"OFFLINE\";\n if (lowerError.includes(\"initializing\")) return \"INITIALIZING\";\n if (lowerError.includes(\"booting\")) return \"BOOTING\";\n if (lowerError.includes(\"waiting for data\")) return \"WAITING_FOR_DATA\";\n if (lowerError.includes(\"shutting down\")) return \"SHUTTING_DOWN\";\n if (lowerError.includes(\"invalid\")) return \"INVALID\";\n\n return \"ERROR\";\n}\n\n/**\n * Get human-readable message for stream status.\n */\nfunction getStatusMessage(status: StreamStatus, percentage?: number): string {\n switch (status) {\n case \"ONLINE\":\n return \"Stream is online\";\n case \"OFFLINE\":\n return \"Stream is offline\";\n case \"INITIALIZING\":\n return percentage !== undefined\n ? `Initializing... ${Math.round(percentage * 10) / 10}%`\n : \"Stream is initializing\";\n case \"BOOTING\":\n return \"Stream is starting up\";\n case \"WAITING_FOR_DATA\":\n return \"Waiting for stream data\";\n case \"SHUTTING_DOWN\":\n return \"Stream is shutting down\";\n case \"INVALID\":\n return \"Stream status is invalid\";\n case \"ERROR\":\n default:\n return \"Stream error\";\n }\n}\n\n// ============================================================================\n// StreamStateClient Class\n// ============================================================================\n\n/**\n * Client for polling MistServer stream status via WebSocket or HTTP.\n *\n * @example\n * ```typescript\n * const client = new StreamStateClient({\n * mistBaseUrl: 'https://mist.example.com',\n * streamName: 'pk_...', // playbackId (view key)\n * });\n *\n * client.on('stateChange', ({ state }) => console.log('State:', state));\n * client.on('online', () => console.log('Stream is online!'));\n * client.on('offline', () => console.log('Stream is offline'));\n *\n * client.start();\n * // ...later\n * client.stop();\n * ```\n */\nexport class StreamStateClient extends TypedEventEmitter<StreamStateClientEvents> {\n private config: StreamStateClientResolvedConfig;\n private state: StreamState = { ...initialState };\n private ws: WebSocket | null = null;\n private timers = new TimerManager();\n private isRunning: boolean = false;\n private wasOnline: boolean = false;\n private connectionId: number = 0; // Track connection attempts to prevent stale callbacks\n\n // Debounce time for rapid mount/unmount cycles (ms)\n private static readonly CONNECTION_DEBOUNCE_MS = 100;\n\n constructor(config: StreamStateClientConfig) {\n super();\n this.config = {\n pollInterval: DEFAULT_POLL_INTERVAL,\n useWebSocket: true,\n ...config,\n };\n }\n\n /**\n * Start polling/WebSocket connection.\n * Always does initial HTTP poll to get full stream info (including sources),\n * then connects WebSocket for real-time status updates.\n *\n * Debounced to prevent orphaned connections during rapid mount/unmount cycles.\n */\n start(): void {\n if (this.isRunning) return;\n this.isRunning = true;\n\n const { mistBaseUrl, streamName, useWebSocket } = this.config;\n\n if (!mistBaseUrl || !streamName) {\n console.warn(\"[StreamStateClient] Missing mistBaseUrl or streamName\");\n return;\n }\n\n // Reset state\n this.setState({\n ...initialState,\n message: \"Connecting...\",\n lastUpdate: Date.now(),\n });\n\n // Increment connection ID to invalidate any pending callbacks from previous attempts\n const currentConnectionId = ++this.connectionId;\n\n // Debounce connection to prevent rapid reconnects during mount/unmount cycles\n this.timers.start(\n () => {\n // Check if this connection attempt is still valid\n if (!this.isRunning || this.connectionId !== currentConnectionId) {\n return;\n }\n\n // Always do initial HTTP poll to get full data (including sources)\n // Then connect WebSocket for real-time updates\n this.pollHttp().then(() => {\n // Verify still valid before WebSocket connection\n if (useWebSocket && this.isRunning && this.connectionId === currentConnectionId) {\n this.connectWebSocket();\n }\n });\n },\n StreamStateClient.CONNECTION_DEBOUNCE_MS,\n \"connect\"\n );\n }\n\n /**\n * Stop polling and close connections.\n */\n stop(): void {\n this.isRunning = false;\n\n // Close WebSocket\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n\n // Clear all timers\n this.timers.destroy();\n }\n\n /**\n * Manual refresh - trigger an immediate poll.\n */\n refresh(): void {\n if (this.config.useWebSocket && this.ws?.readyState === WebSocket.OPEN) {\n // WebSocket will receive updates automatically\n return;\n }\n this.pollHttp();\n }\n\n /**\n * Get the underlying WebSocket connection (for MistReporter integration).\n * Returns null if WebSocket is not connected.\n */\n getSocket(): WebSocket | null {\n return this.ws;\n }\n\n /**\n * Check if the WebSocket is connected and ready.\n */\n isSocketReady(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Get current stream state.\n */\n getState(): StreamState {\n return { ...this.state };\n }\n\n /**\n * Check if stream is online.\n */\n isOnline(): boolean {\n return this.state.isOnline;\n }\n\n /**\n * Update configuration (stops and restarts if running).\n */\n updateConfig(config: Partial<StreamStateClientConfig>): void {\n const wasRunning = this.isRunning;\n this.stop();\n this.config = { ...this.config, ...config };\n if (wasRunning) {\n this.start();\n }\n }\n\n /**\n * Clean up resources.\n */\n destroy(): void {\n this.stop();\n this.removeAllListeners();\n }\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private connectWebSocket(): void {\n if (!this.isRunning) return;\n\n const { mistBaseUrl, streamName } = this.config;\n\n // Clean up existing connection\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n\n try {\n // Convert http(s) to ws(s)\n const wsUrl = mistBaseUrl\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\")\n .replace(/\\/$/, \"\");\n\n const ws = new WebSocket(\n `${wsUrl}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`\n );\n this.ws = ws;\n\n ws.onopen = () => {\n console.debug(\"[StreamStateClient] WebSocket connected\");\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data) as MistStreamInfo;\n this.processStreamInfo(data);\n } catch (e) {\n console.warn(\"[StreamStateClient] Failed to parse WebSocket message:\", e);\n }\n };\n\n ws.onerror = () => {\n console.warn(\"[StreamStateClient] WebSocket error, falling back to HTTP polling\");\n ws.close();\n };\n\n ws.onclose = () => {\n this.ws = null;\n\n if (!this.isRunning) return;\n\n // Disable WebSocket and switch to HTTP polling\n // This ensures pollHttp() schedules repeat polls (see line 365 condition)\n this.config.useWebSocket = false;\n console.debug(\"[StreamStateClient] WebSocket closed, switching to HTTP polling\");\n this.pollHttp();\n };\n } catch (error) {\n console.warn(\"[StreamStateClient] WebSocket connection failed:\", error);\n // Disable WebSocket and switch to HTTP polling\n this.config.useWebSocket = false;\n this.pollHttp();\n }\n }\n\n private async pollHttp(): Promise<void> {\n if (!this.isRunning) return;\n\n const { mistBaseUrl, streamName, pollInterval } = this.config;\n\n try {\n const url = `${mistBaseUrl.replace(/\\/$/, \"\")}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n // MistServer returns JSON with potential JSONP wrapper\n let text = await response.text();\n // Strip JSONP callback if present\n const jsonpMatch = text.match(/^[^(]+\\(([\\s\\S]*)\\);?$/);\n if (jsonpMatch) {\n text = jsonpMatch[1];\n }\n\n const data = JSON.parse(text) as MistStreamInfo;\n this.processStreamInfo(data);\n } catch (error) {\n if (!this.isRunning) return;\n\n const errorMessage = error instanceof Error ? error.message : \"Connection failed\";\n this.setState({\n ...this.state,\n status: \"ERROR\",\n isOnline: false,\n message: errorMessage,\n lastUpdate: Date.now(),\n error: errorMessage,\n });\n this.emit(\"error\", { error: errorMessage });\n }\n\n // Schedule next poll\n if (this.isRunning && !this.config.useWebSocket) {\n this.timers.start(() => this.pollHttp(), pollInterval, \"poll\");\n }\n }\n\n private processStreamInfo(data: MistStreamInfo): void {\n if (!this.isRunning) return;\n\n let newState: StreamState;\n\n if (data.error) {\n // Stream has an error state - preserve existing streamInfo\n const status = parseErrorToStatus(data.error);\n const message = data.on_error || getStatusMessage(status, data.perc);\n\n newState = {\n status,\n isOnline: false,\n message,\n percentage: data.perc,\n lastUpdate: Date.now(),\n error: data.error,\n streamInfo: this.state.streamInfo, // Preserve existing source/tracks\n };\n } else {\n // Stream is online with valid metadata\n // Merge new data with existing streamInfo to preserve source/tracks from initial fetch\n // WebSocket updates may not include source array\n const mergedStreamInfo: MistStreamInfo = {\n ...this.state.streamInfo, // Keep existing source/meta if present\n ...data, // Override with new data\n // Explicitly preserve source if not in new data\n source: data.source || this.state.streamInfo?.source,\n // Merge meta to preserve tracks\n meta: {\n ...this.state.streamInfo?.meta,\n ...data.meta,\n // Preserve tracks if not in new data\n tracks: data.meta?.tracks || this.state.streamInfo?.meta?.tracks,\n },\n };\n\n newState = {\n status: \"ONLINE\",\n isOnline: true,\n message: \"Stream is online\",\n lastUpdate: Date.now(),\n streamInfo: mergedStreamInfo,\n };\n }\n\n this.setState(newState);\n\n // Emit online/offline events on state transitions\n if (newState.isOnline && !this.wasOnline) {\n this.emit(\"online\", undefined as never);\n } else if (!newState.isOnline && this.wasOnline) {\n this.emit(\"offline\", undefined as never);\n }\n this.wasOnline = newState.isOnline;\n }\n\n private setState(state: StreamState): void {\n const prevState = this.state;\n this.state = state;\n\n // Emit if ANY state field changed - including streamInfo (track data)\n // Previously only checked status/isOnline/message, causing track updates to be lost\n const hasChanged =\n prevState.status !== state.status ||\n prevState.isOnline !== state.isOnline ||\n prevState.message !== state.message ||\n prevState.streamInfo !== state.streamInfo ||\n prevState.lastUpdate !== state.lastUpdate;\n\n if (hasChanged) {\n this.emit(\"stateChange\", { state });\n }\n }\n}\n\nexport default StreamStateClient;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubtitleManager - WebVTT subtitle track management
|
|
3
|
+
*
|
|
4
|
+
* Based on MistMetaPlayer's subtitle handling (wrappers/html5.js, webrtc.js).
|
|
5
|
+
* Manages text tracks on video elements with support for:
|
|
6
|
+
* - Loading WebVTT from MistServer URLs
|
|
7
|
+
* - Multiple subtitle track selection
|
|
8
|
+
* - Sync correction for WebRTC seek offsets
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* SubtitleManager handles text track lifecycle on a video element
|
|
12
|
+
*/
|
|
13
|
+
class SubtitleManager {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.video = null;
|
|
16
|
+
this.currentTrackId = null;
|
|
17
|
+
this.seekOffset = 0;
|
|
18
|
+
this.listeners = [];
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.debug = config.debug ?? false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Attach to a video element
|
|
24
|
+
*/
|
|
25
|
+
attach(video) {
|
|
26
|
+
this.detach();
|
|
27
|
+
this.video = video;
|
|
28
|
+
// Listen for events that may require sync correction
|
|
29
|
+
const onLoadedData = () => this.correctSubtitleSync();
|
|
30
|
+
const onSeeked = () => this.correctSubtitleSync();
|
|
31
|
+
video.addEventListener("loadeddata", onLoadedData);
|
|
32
|
+
video.addEventListener("seeked", onSeeked);
|
|
33
|
+
this.listeners = [
|
|
34
|
+
() => video.removeEventListener("loadeddata", onLoadedData),
|
|
35
|
+
() => video.removeEventListener("seeked", onSeeked),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Detach from video element
|
|
40
|
+
*/
|
|
41
|
+
detach() {
|
|
42
|
+
this.listeners.forEach((fn) => fn());
|
|
43
|
+
this.listeners = [];
|
|
44
|
+
this.removeAllTracks();
|
|
45
|
+
this.video = null;
|
|
46
|
+
this.currentTrackId = null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get available text tracks from the video element
|
|
50
|
+
*/
|
|
51
|
+
getTextTracks() {
|
|
52
|
+
if (!this.video)
|
|
53
|
+
return [];
|
|
54
|
+
return Array.from(this.video.textTracks);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get all track elements from the video
|
|
58
|
+
*/
|
|
59
|
+
getTrackElements() {
|
|
60
|
+
if (!this.video)
|
|
61
|
+
return [];
|
|
62
|
+
return Array.from(this.video.querySelectorAll("track"));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Set the active subtitle track
|
|
66
|
+
* Pass null to disable subtitles
|
|
67
|
+
*/
|
|
68
|
+
setSubtitle(track) {
|
|
69
|
+
if (!this.video) {
|
|
70
|
+
this.log("Cannot set subtitle: no video element attached");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Remove existing subtitle tracks
|
|
74
|
+
this.removeAllTracks();
|
|
75
|
+
if (!track) {
|
|
76
|
+
this.currentTrackId = null;
|
|
77
|
+
this.log("Subtitles disabled");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Create new track element
|
|
81
|
+
const trackElement = document.createElement("track");
|
|
82
|
+
trackElement.kind = "subtitles";
|
|
83
|
+
trackElement.label = track.label;
|
|
84
|
+
trackElement.srclang = track.lang;
|
|
85
|
+
trackElement.src = this.buildTrackUrl(track.src);
|
|
86
|
+
trackElement.default = true;
|
|
87
|
+
// Set up load handler for sync correction
|
|
88
|
+
trackElement.addEventListener("load", () => {
|
|
89
|
+
this.correctSubtitleSync();
|
|
90
|
+
});
|
|
91
|
+
this.video.appendChild(trackElement);
|
|
92
|
+
this.currentTrackId = track.id;
|
|
93
|
+
// Enable the track
|
|
94
|
+
const textTrack = this.video.textTracks[this.video.textTracks.length - 1];
|
|
95
|
+
if (textTrack) {
|
|
96
|
+
textTrack.mode = "showing";
|
|
97
|
+
}
|
|
98
|
+
this.log(`Subtitle track set: ${track.label} (${track.lang})`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build track URL with base URL and append params
|
|
102
|
+
*/
|
|
103
|
+
buildTrackUrl(src) {
|
|
104
|
+
let url = src;
|
|
105
|
+
// If relative URL and base URL provided, construct full URL
|
|
106
|
+
if (!url.startsWith("http") && this.config.mistBaseUrl) {
|
|
107
|
+
const base = this.config.mistBaseUrl.replace(/\/$/, "");
|
|
108
|
+
url = url.startsWith("/") ? `${base}${url}` : `${base}/${url}`;
|
|
109
|
+
}
|
|
110
|
+
// Append URL params if configured
|
|
111
|
+
if (this.config.urlAppend) {
|
|
112
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
113
|
+
url = `${url}${separator}${this.config.urlAppend}`;
|
|
114
|
+
}
|
|
115
|
+
return url;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create subtitle track info from MistServer track metadata
|
|
119
|
+
*/
|
|
120
|
+
static createTrackInfo(trackId, label, lang, baseUrl, streamName) {
|
|
121
|
+
// MistServer WebVTT URL format
|
|
122
|
+
const src = `${baseUrl}/${streamName}.vtt?track=${trackId}`;
|
|
123
|
+
return { id: trackId, label, lang, src };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Remove all track elements from video
|
|
127
|
+
*/
|
|
128
|
+
removeAllTracks() {
|
|
129
|
+
if (!this.video)
|
|
130
|
+
return;
|
|
131
|
+
const tracks = this.video.querySelectorAll("track");
|
|
132
|
+
tracks.forEach((track) => track.remove());
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get currently active track ID
|
|
136
|
+
*/
|
|
137
|
+
getCurrentTrackId() {
|
|
138
|
+
return this.currentTrackId;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Set seek offset for WebRTC sync correction
|
|
142
|
+
* WebRTC playback has a seek offset that needs to be applied to subtitle timing
|
|
143
|
+
*/
|
|
144
|
+
setSeekOffset(offset) {
|
|
145
|
+
const oldOffset = this.seekOffset;
|
|
146
|
+
this.seekOffset = offset;
|
|
147
|
+
// Re-sync if offset changed significantly
|
|
148
|
+
if (Math.abs(oldOffset - offset) > 1) {
|
|
149
|
+
this.correctSubtitleSync();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Correct subtitle timing based on seek offset
|
|
154
|
+
* This is needed for WebRTC where video.currentTime doesn't match actual playback position
|
|
155
|
+
*/
|
|
156
|
+
correctSubtitleSync() {
|
|
157
|
+
if (!this.video || this.video.textTracks.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const textTrack = this.video.textTracks[0];
|
|
160
|
+
if (!textTrack || !textTrack.cues)
|
|
161
|
+
return;
|
|
162
|
+
const currentOffset = textTrack.currentOffset || 0;
|
|
163
|
+
// Don't bother if change is small
|
|
164
|
+
if (Math.abs(this.seekOffset - currentOffset) < 1)
|
|
165
|
+
return;
|
|
166
|
+
this.log(`Correcting subtitle sync: offset ${currentOffset} -> ${this.seekOffset}`);
|
|
167
|
+
// Collect and re-add cues with corrected timing
|
|
168
|
+
const newCues = [];
|
|
169
|
+
for (let i = textTrack.cues.length - 1; i >= 0; i--) {
|
|
170
|
+
const cue = textTrack.cues[i];
|
|
171
|
+
textTrack.removeCue(cue);
|
|
172
|
+
// Store original timing if not already stored
|
|
173
|
+
if (!cue.orig) {
|
|
174
|
+
cue.orig = { start: cue.startTime, end: cue.endTime };
|
|
175
|
+
}
|
|
176
|
+
// Apply offset correction
|
|
177
|
+
cue.startTime = cue.orig.start - this.seekOffset;
|
|
178
|
+
cue.endTime = cue.orig.end - this.seekOffset;
|
|
179
|
+
newCues.push(cue);
|
|
180
|
+
}
|
|
181
|
+
// Re-add cues
|
|
182
|
+
for (const cue of newCues) {
|
|
183
|
+
try {
|
|
184
|
+
textTrack.addCue(cue);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Ignore errors from invalid cue timing
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
textTrack.currentOffset = this.seekOffset;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Parse subtitle tracks from MistServer stream info
|
|
194
|
+
*/
|
|
195
|
+
static parseTracksFromStreamInfo(streamInfo, baseUrl, streamName) {
|
|
196
|
+
const tracks = [];
|
|
197
|
+
if (!streamInfo.meta?.tracks)
|
|
198
|
+
return tracks;
|
|
199
|
+
for (const [trackId, trackData] of Object.entries(streamInfo.meta.tracks)) {
|
|
200
|
+
if (trackData.type === "meta" && trackData.codec === "subtitle") {
|
|
201
|
+
const lang = trackData.lang || "und";
|
|
202
|
+
const label = lang === "und" ? `Subtitles ${trackId}` : lang.toUpperCase();
|
|
203
|
+
tracks.push(SubtitleManager.createTrackInfo(trackId, label, lang, baseUrl, streamName));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return tracks;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Debug logging
|
|
210
|
+
*/
|
|
211
|
+
log(message) {
|
|
212
|
+
if (this.debug) {
|
|
213
|
+
console.debug(`[SubtitleManager] ${message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Cleanup
|
|
218
|
+
*/
|
|
219
|
+
destroy() {
|
|
220
|
+
this.detach();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export { SubtitleManager };
|
|
225
|
+
//# sourceMappingURL=SubtitleManager.js.map
|