@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,1065 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MEWS WebSocket Player Implementation
|
|
3
|
+
*
|
|
4
|
+
* Low-latency WebSocket MP4 streaming using MediaSource Extensions.
|
|
5
|
+
* Protocol: Custom MEWS (MistServer Extended WebSocket)
|
|
6
|
+
*
|
|
7
|
+
* Ported from reference: mews.js (MistMetaPlayer)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BasePlayer } from '../../core/PlayerInterface';
|
|
11
|
+
import type { StreamSource, StreamInfo, PlayerOptions, PlayerCapability } from '../../core/PlayerInterface';
|
|
12
|
+
import { WebSocketManager } from './WebSocketManager';
|
|
13
|
+
import { SourceBufferManager } from './SourceBufferManager';
|
|
14
|
+
import { translateCodec } from '../../core/CodecUtils';
|
|
15
|
+
import type { MewsMessage, AnalyticsConfig, OnTimeMessage, MewsMessageListener } from './types';
|
|
16
|
+
|
|
17
|
+
export class MewsWsPlayerImpl extends BasePlayer {
|
|
18
|
+
readonly capability: PlayerCapability = {
|
|
19
|
+
name: "MEWS WebSocket Player",
|
|
20
|
+
shortname: "mews",
|
|
21
|
+
priority: 2, // High priority - low latency protocol
|
|
22
|
+
mimes: ["ws/video/mp4", "wss/video/mp4", "ws/video/webm", "wss/video/webm"]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
private wsManager: WebSocketManager | null = null;
|
|
26
|
+
private sbManager: SourceBufferManager | null = null;
|
|
27
|
+
private mediaSource: MediaSource | null = null;
|
|
28
|
+
private objectUrl: string | null = null;
|
|
29
|
+
private container: HTMLElement | null = null;
|
|
30
|
+
private isDestroyed = false;
|
|
31
|
+
private debugging = false;
|
|
32
|
+
|
|
33
|
+
// Server delay estimation (ported from mews.js:833-882)
|
|
34
|
+
private serverDelays: number[] = [];
|
|
35
|
+
private pendingDelayTypes: Record<string, number> = {};
|
|
36
|
+
|
|
37
|
+
// Supported codecs (short names for MistServer protocol)
|
|
38
|
+
private supportedCodecs: string[] = [];
|
|
39
|
+
|
|
40
|
+
// Ready state - true after codec_data received and SourceBuffer initialized
|
|
41
|
+
private isReady = false;
|
|
42
|
+
private readyResolvers: Array<() => void> = [];
|
|
43
|
+
|
|
44
|
+
// Duration tracking (ported from mews.js:1113)
|
|
45
|
+
private lastDuration = Infinity;
|
|
46
|
+
|
|
47
|
+
// Live vs VoD detection (ported from mews.js:105-107, 508)
|
|
48
|
+
private streamType: 'live' | 'vod' | 'unknown' = 'unknown';
|
|
49
|
+
|
|
50
|
+
// Current tracks for change detection (ported from mews.js:455, 593-619)
|
|
51
|
+
private currentTracks: string[] = [];
|
|
52
|
+
|
|
53
|
+
// Last codecs for track switch comparison (ported from mews.js:687)
|
|
54
|
+
private lastCodecs: string[] | null = null;
|
|
55
|
+
|
|
56
|
+
// Playback rate tuning (ported from mews.js:453, 509-545)
|
|
57
|
+
private requestedRate = 1;
|
|
58
|
+
|
|
59
|
+
// ABR state (ported from mews.js:1266-1314)
|
|
60
|
+
private bitCounter: number[] = [];
|
|
61
|
+
private bitsSince: number[] = [];
|
|
62
|
+
private currentBps: number | null = null;
|
|
63
|
+
private nWaiting = 0;
|
|
64
|
+
private nWaitingThreshold = 3;
|
|
65
|
+
|
|
66
|
+
// Seeking state (ported from mews.js:1169-1175)
|
|
67
|
+
private seeking = false;
|
|
68
|
+
|
|
69
|
+
// Analytics
|
|
70
|
+
private analyticsConfig: AnalyticsConfig = { enabled: false, endpoint: null };
|
|
71
|
+
private analyticsTimer: ReturnType<typeof setInterval> | null = null;
|
|
72
|
+
|
|
73
|
+
isMimeSupported(mimetype: string): boolean {
|
|
74
|
+
return this.capability.mimes.includes(mimetype);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isBrowserSupported(mimetype: string, source: StreamSource, streamInfo: StreamInfo): boolean | string[] {
|
|
78
|
+
// Basic requirements check (mews.js:10)
|
|
79
|
+
if (!('WebSocket' in window) || !('MediaSource' in window) || !('Promise' in window)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MacOS exemption (reference mews.js behavior)
|
|
84
|
+
// MediaSource has bugs on Safari/MacOS - prefer HLS
|
|
85
|
+
const isMac = /Mac OS X/.test(navigator.userAgent);
|
|
86
|
+
if (isMac) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check codec compatibility using ACTUAL stream codecs (mews.js:45-83)
|
|
91
|
+
const container = mimetype.split('/')[2] || 'mp4';
|
|
92
|
+
const playableTracks: Record<string, number> = {};
|
|
93
|
+
let hasSubtitles = false;
|
|
94
|
+
|
|
95
|
+
// Test actual stream codecs against MediaSource
|
|
96
|
+
this.supportedCodecs = [];
|
|
97
|
+
for (const track of streamInfo.meta.tracks) {
|
|
98
|
+
if (track.type === 'meta') {
|
|
99
|
+
if (track.codec === 'subtitle') hasSubtitles = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const codecString = translateCodec(track as any);
|
|
104
|
+
const testMime = `video/${container};codecs="${codecString}"`;
|
|
105
|
+
|
|
106
|
+
if (MediaSource.isTypeSupported(testMime)) {
|
|
107
|
+
this.supportedCodecs.push(track.codec);
|
|
108
|
+
playableTracks[track.type] = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for subtitle source (mews.js:73-80)
|
|
113
|
+
if (hasSubtitles) {
|
|
114
|
+
const hasVttSource = streamInfo.source?.some(s => s.type === 'html5/text/vtt');
|
|
115
|
+
if (hasVttSource) {
|
|
116
|
+
playableTracks['subtitle'] = 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Object.keys(playableTracks).length === 0) return false;
|
|
121
|
+
return Object.keys(playableTracks);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions): Promise<HTMLVideoElement> {
|
|
125
|
+
this.container = container;
|
|
126
|
+
container.classList.add('fw-player-container');
|
|
127
|
+
|
|
128
|
+
const video = document.createElement('video');
|
|
129
|
+
video.classList.add('fw-player-video');
|
|
130
|
+
video.setAttribute('playsinline', ''); // iphones (mews.js:92)
|
|
131
|
+
video.setAttribute('crossorigin', 'anonymous'); // mews.js:111
|
|
132
|
+
|
|
133
|
+
// Apply options (mews.js:95-110)
|
|
134
|
+
if (options.autoplay) video.autoplay = true;
|
|
135
|
+
if (options.muted) video.muted = true;
|
|
136
|
+
video.controls = options.controls === true;
|
|
137
|
+
if (options.loop) video.loop = true;
|
|
138
|
+
if (options.poster) video.poster = options.poster;
|
|
139
|
+
|
|
140
|
+
// Live streams don't loop (mews.js:105-107)
|
|
141
|
+
if (this.streamType === 'live') {
|
|
142
|
+
video.loop = false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.videoElement = video;
|
|
146
|
+
container.appendChild(video);
|
|
147
|
+
this.setupVideoEventListeners(video, options);
|
|
148
|
+
|
|
149
|
+
// Analytics configuration
|
|
150
|
+
const anyOpts = options as any;
|
|
151
|
+
this.analyticsConfig = {
|
|
152
|
+
enabled: !!anyOpts.analytics?.enabled,
|
|
153
|
+
endpoint: anyOpts.analytics?.endpoint || null
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Get stream type from options if available
|
|
157
|
+
if ((source as any).type === 'live') {
|
|
158
|
+
this.streamType = 'live';
|
|
159
|
+
} else if ((source as any).type === 'vod') {
|
|
160
|
+
this.streamType = 'vod';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Initialize MediaSource (mews.js:138-196)
|
|
165
|
+
this.mediaSource = new MediaSource();
|
|
166
|
+
|
|
167
|
+
// Set up MediaSource event handlers (mews.js:143-195)
|
|
168
|
+
this.mediaSource.addEventListener('sourceopen', () => this.handleSourceOpen(source));
|
|
169
|
+
this.mediaSource.addEventListener('sourceclose', () => this.handleSourceClose());
|
|
170
|
+
this.mediaSource.addEventListener('sourceended', () => this.handleSourceEnded());
|
|
171
|
+
|
|
172
|
+
this.objectUrl = URL.createObjectURL(this.mediaSource);
|
|
173
|
+
video.src = this.objectUrl;
|
|
174
|
+
this.isDestroyed = false;
|
|
175
|
+
this.startTelemetry();
|
|
176
|
+
return video;
|
|
177
|
+
} catch (error: any) {
|
|
178
|
+
this.emit('error', error.message || String(error));
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle MediaSource sourceopen event.
|
|
185
|
+
* Ported from mews.js:143-148, 198-204, 885-902
|
|
186
|
+
*/
|
|
187
|
+
private handleSourceOpen(source: StreamSource): void {
|
|
188
|
+
if (!this.mediaSource || !this.videoElement) return;
|
|
189
|
+
|
|
190
|
+
// Create SourceBufferManager
|
|
191
|
+
this.sbManager = new SourceBufferManager({
|
|
192
|
+
mediaSource: this.mediaSource,
|
|
193
|
+
videoElement: this.videoElement,
|
|
194
|
+
onError: (msg) => this.emit('error', msg)
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Install browser event handlers
|
|
198
|
+
this.installWaitingHandler();
|
|
199
|
+
this.installSeekingHandler();
|
|
200
|
+
this.installPauseHandler();
|
|
201
|
+
this.installLoopHandler();
|
|
202
|
+
|
|
203
|
+
// Create WebSocketManager with listener support
|
|
204
|
+
this.wsManager = new WebSocketManager({
|
|
205
|
+
url: source.url,
|
|
206
|
+
maxReconnectAttempts: 5,
|
|
207
|
+
onMessage: (data) => this.handleMessage(data),
|
|
208
|
+
onOpen: () => this.handleWsOpen(),
|
|
209
|
+
onClose: () => this.handleWsClose(),
|
|
210
|
+
onError: (msg) => this.emit('error', msg)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this.wsManager.connect();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle MediaSource sourceclose event.
|
|
218
|
+
* Ported from mews.js:150-153
|
|
219
|
+
*/
|
|
220
|
+
private handleSourceClose(): void {
|
|
221
|
+
if (this.debugging) console.log('MEWS: MediaSource closed');
|
|
222
|
+
this.send({ type: 'stop' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Handle MediaSource sourceended event.
|
|
227
|
+
* Ported from mews.js:154-194
|
|
228
|
+
*/
|
|
229
|
+
private handleSourceEnded(): void {
|
|
230
|
+
if (this.debugging) console.log('MEWS: MediaSource ended');
|
|
231
|
+
this.send({ type: 'stop' });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle WebSocket open event.
|
|
236
|
+
* Ported from mews.js:401-403, 885-902
|
|
237
|
+
*/
|
|
238
|
+
private handleWsOpen(): void {
|
|
239
|
+
// Request codec data (mews.js:885-902)
|
|
240
|
+
const listener: MewsMessageListener = (msg) => {
|
|
241
|
+
// Got codec data, set up source buffer
|
|
242
|
+
if (this.mediaSource?.readyState === 'open') {
|
|
243
|
+
const codecs = msg.data?.codecs || [];
|
|
244
|
+
const initialized = this.sbManager?.initWithCodecs(codecs);
|
|
245
|
+
|
|
246
|
+
if (initialized && !this.isReady) {
|
|
247
|
+
this.isReady = true;
|
|
248
|
+
// Resolve any waiting play() calls
|
|
249
|
+
for (const resolve of this.readyResolvers) {
|
|
250
|
+
resolve();
|
|
251
|
+
}
|
|
252
|
+
this.readyResolvers = [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.wsManager?.removeListener('codec_data', listener);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
this.wsManager?.addListener('codec_data', listener);
|
|
259
|
+
this.logDelay('codec_data');
|
|
260
|
+
|
|
261
|
+
// Send request with SHORT codec names (mews.js:901)
|
|
262
|
+
// CRITICAL: MistServer expects short names like "H264", not browser codec strings
|
|
263
|
+
this.send({ type: 'request_codec_data', supported_codecs: this.supportedCodecs });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Handle WebSocket close event with reconnection logic.
|
|
268
|
+
* Ported from mews.js:408-431
|
|
269
|
+
*/
|
|
270
|
+
private handleWsClose(): void {
|
|
271
|
+
if (this.debugging) console.log('MEWS: WebSocket closed');
|
|
272
|
+
// Reconnection is handled by WebSocketManager
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle incoming WebSocket message.
|
|
277
|
+
* Routes to binary append or JSON control message handler.
|
|
278
|
+
* Ported from mews.js:456-830
|
|
279
|
+
*/
|
|
280
|
+
private handleMessage(data: ArrayBuffer | string): void {
|
|
281
|
+
if (typeof data === 'string') {
|
|
282
|
+
try {
|
|
283
|
+
const msg = JSON.parse(data) as MewsMessage;
|
|
284
|
+
this.handleControlMessage(msg);
|
|
285
|
+
// Notify listeners (mews.js:795-799)
|
|
286
|
+
this.wsManager?.notifyListeners(msg);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (this.debugging) console.error('MEWS: Failed to parse message', e);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Binary data - MP4 segment (mews.js:802-829)
|
|
294
|
+
const bytes = new Uint8Array(data);
|
|
295
|
+
this.sbManager?.append(bytes);
|
|
296
|
+
this.trackBits(data);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Handle JSON control messages.
|
|
301
|
+
* Ported from mews.js:461-799
|
|
302
|
+
*/
|
|
303
|
+
private handleControlMessage(msg: MewsMessage): void {
|
|
304
|
+
if (this.debugging && msg.type !== 'on_time') {
|
|
305
|
+
console.log('MEWS: message', msg);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
switch (msg.type) {
|
|
309
|
+
case 'on_stop':
|
|
310
|
+
this.handleOnStop();
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'on_time':
|
|
314
|
+
this.handleOnTime(msg as OnTimeMessage);
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'tracks':
|
|
318
|
+
this.handleTracks(msg);
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'pause':
|
|
322
|
+
this.handlePause();
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case 'codec_data':
|
|
326
|
+
this.resolveDelay('codec_data');
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'seek':
|
|
330
|
+
this.resolveDelay('seek');
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
case 'set_speed':
|
|
334
|
+
this.resolveDelay('set_speed');
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Handle on_stop message - stream ended (VoD).
|
|
341
|
+
* Ported from mews.js:462-471
|
|
342
|
+
*/
|
|
343
|
+
private handleOnStop(): void {
|
|
344
|
+
// Mark as VoD (stream ended)
|
|
345
|
+
this.streamType = 'vod';
|
|
346
|
+
|
|
347
|
+
// Wait for buffer to finish playing (mews.js:465-469)
|
|
348
|
+
const onWaiting = () => {
|
|
349
|
+
if (this.sbManager) {
|
|
350
|
+
this.sbManager.paused = true;
|
|
351
|
+
}
|
|
352
|
+
this.emit('ended', undefined);
|
|
353
|
+
this.videoElement?.removeEventListener('waiting', onWaiting);
|
|
354
|
+
};
|
|
355
|
+
this.videoElement?.addEventListener('waiting', onWaiting);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Handle on_time message - playback time sync.
|
|
360
|
+
* Ported from mews.js:473-621
|
|
361
|
+
*/
|
|
362
|
+
private handleOnTime(msg: OnTimeMessage): void {
|
|
363
|
+
const data = msg.data;
|
|
364
|
+
if (!data || !this.videoElement) return;
|
|
365
|
+
|
|
366
|
+
const currentMs = data.current;
|
|
367
|
+
const endMs = data.end;
|
|
368
|
+
const jitter = data.jitter || 0;
|
|
369
|
+
|
|
370
|
+
// Buffer calculation (mews.js:474)
|
|
371
|
+
const buffer = currentMs - this.videoElement.currentTime * 1000;
|
|
372
|
+
const serverDelay = this.getServerDelay();
|
|
373
|
+
// Chrome needs larger base buffer (mews.js:482)
|
|
374
|
+
const isChrome = /Chrome/.test(navigator.userAgent) && !/Edge|Edg/.test(navigator.userAgent);
|
|
375
|
+
const baseBuffer = isChrome ? 1000 : 100;
|
|
376
|
+
const desiredBuffer = Math.max(baseBuffer + serverDelay, serverDelay * 2);
|
|
377
|
+
const desiredBufferWithJitter = desiredBuffer + jitter;
|
|
378
|
+
|
|
379
|
+
// VoD gets extra buffer (mews.js:480)
|
|
380
|
+
const actualDesiredBuffer = this.streamType !== 'live' ? desiredBuffer + 2000 : desiredBuffer;
|
|
381
|
+
|
|
382
|
+
if (this.debugging) {
|
|
383
|
+
console.log(
|
|
384
|
+
'MEWS: on_time',
|
|
385
|
+
'current:', currentMs / 1000,
|
|
386
|
+
'video:', this.videoElement.currentTime,
|
|
387
|
+
'rate:', this.requestedRate + 'x',
|
|
388
|
+
'buffer:', Math.round(buffer), '/', Math.round(desiredBuffer),
|
|
389
|
+
this.streamType === 'live' ? 'latency:' + Math.round((endMs || 0) - this.videoElement.currentTime * 1000) + 'ms' : ''
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!this.sbManager) {
|
|
394
|
+
if (this.debugging) console.log('MEWS: on_time but no sourceBuffer');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Update duration (mews.js:501-504)
|
|
399
|
+
if (endMs !== undefined && this.lastDuration !== endMs / 1000) {
|
|
400
|
+
this.lastDuration = endMs / 1000;
|
|
401
|
+
// Duration is updated via native video element durationchange event
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Mark source buffer as not paused
|
|
405
|
+
this.sbManager.paused = false;
|
|
406
|
+
|
|
407
|
+
// Playback rate tuning for LIVE streams (mews.js:508-545)
|
|
408
|
+
if (this.streamType === 'live') {
|
|
409
|
+
this.tuneLivePlaybackRate(buffer, desiredBufferWithJitter, data.play_rate_curr);
|
|
410
|
+
} else {
|
|
411
|
+
// VoD - adjust server delivery speed (mews.js:547-586)
|
|
412
|
+
this.tuneVodDeliverySpeed(buffer, actualDesiredBuffer, data.play_rate_curr);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Track change detection (mews.js:593-619)
|
|
416
|
+
if (data.tracks && this.currentTracks.join(',') !== data.tracks.join(',')) {
|
|
417
|
+
if (this.debugging) {
|
|
418
|
+
for (const trackId of data.tracks) {
|
|
419
|
+
if (!this.currentTracks.includes(trackId)) {
|
|
420
|
+
console.log('MEWS: track changed', trackId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.currentTracks = data.tracks;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Tune playback rate for live streams.
|
|
430
|
+
* Ported from mews.js:508-545
|
|
431
|
+
*
|
|
432
|
+
* Fixed: Use direct assignment instead of multiplication to prevent
|
|
433
|
+
* compounding rate adjustments on each on_time message.
|
|
434
|
+
*/
|
|
435
|
+
private tuneLivePlaybackRate(buffer: number, desiredBuffer: number, playRateCurr?: 'auto' | number): void {
|
|
436
|
+
if (!this.videoElement) return;
|
|
437
|
+
|
|
438
|
+
if (this.requestedRate === 1) {
|
|
439
|
+
if (playRateCurr === 'auto' && this.videoElement.currentTime > 0) {
|
|
440
|
+
// Assume we want to be as live as possible
|
|
441
|
+
if (buffer > desiredBuffer * 2) {
|
|
442
|
+
// Buffer too big, speed up (mews.js:513-516)
|
|
443
|
+
this.requestedRate = 1 + Math.min(1, (buffer - desiredBuffer) / desiredBuffer) * 0.08;
|
|
444
|
+
this.videoElement.playbackRate = this.requestedRate;
|
|
445
|
+
if (this.debugging) console.log('MEWS: speeding up to', this.requestedRate);
|
|
446
|
+
} else if (buffer < 0) {
|
|
447
|
+
// Negative buffer, slow down (mews.js:518-521)
|
|
448
|
+
this.requestedRate = 0.8;
|
|
449
|
+
this.videoElement.playbackRate = this.requestedRate;
|
|
450
|
+
if (this.debugging) console.log('MEWS: slowing down to', this.requestedRate);
|
|
451
|
+
} else if (buffer < desiredBuffer / 2) {
|
|
452
|
+
// Buffer too small, slow down (mews.js:523-526)
|
|
453
|
+
this.requestedRate = 1 + Math.min(1, (buffer - desiredBuffer) / desiredBuffer) * 0.08;
|
|
454
|
+
this.videoElement.playbackRate = this.requestedRate;
|
|
455
|
+
if (this.debugging) console.log('MEWS: adjusting to', this.requestedRate);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} else if (this.requestedRate > 1) {
|
|
459
|
+
// Return to normal when buffer is small enough (mews.js:531-536)
|
|
460
|
+
if (buffer < desiredBuffer) {
|
|
461
|
+
this.videoElement.playbackRate = 1;
|
|
462
|
+
this.requestedRate = 1;
|
|
463
|
+
if (this.debugging) console.log('MEWS: returning to normal rate');
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// requestedRate < 1, return to normal when buffer is big enough (mews.js:538-544)
|
|
467
|
+
if (buffer > desiredBuffer) {
|
|
468
|
+
this.videoElement.playbackRate = 1;
|
|
469
|
+
this.requestedRate = 1;
|
|
470
|
+
if (this.debugging) console.log('MEWS: returning to normal rate');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Tune server delivery speed for VoD.
|
|
477
|
+
* Ported from mews.js:547-586
|
|
478
|
+
*/
|
|
479
|
+
private tuneVodDeliverySpeed(buffer: number, desiredBuffer: number, playRateCurr?: 'auto' | number): void {
|
|
480
|
+
if (this.requestedRate === 1) {
|
|
481
|
+
if (playRateCurr === 'auto') {
|
|
482
|
+
if (buffer < desiredBuffer / 2) {
|
|
483
|
+
if (buffer < -10000) {
|
|
484
|
+
// Way behind, seek to current position (mews.js:553-554)
|
|
485
|
+
this.send({ type: 'seek', seek_time: Math.round((this.videoElement?.currentTime || 0) * 1000) });
|
|
486
|
+
} else {
|
|
487
|
+
// Request faster delivery (mews.js:557-560)
|
|
488
|
+
this.requestedRate = 2;
|
|
489
|
+
this.send({ type: 'set_speed', play_rate: this.requestedRate });
|
|
490
|
+
if (this.debugging) console.log('MEWS: requesting faster delivery');
|
|
491
|
+
}
|
|
492
|
+
} else if (buffer - desiredBuffer > desiredBuffer) {
|
|
493
|
+
// Too much buffer, slow down (mews.js:563-566)
|
|
494
|
+
this.requestedRate = 0.5;
|
|
495
|
+
this.send({ type: 'set_speed', play_rate: this.requestedRate });
|
|
496
|
+
if (this.debugging) console.log('MEWS: requesting slower delivery');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else if (this.requestedRate > 1) {
|
|
500
|
+
if (buffer > desiredBuffer) {
|
|
501
|
+
// Enough buffer, return to realtime (mews.js:571-575)
|
|
502
|
+
this.send({ type: 'set_speed', play_rate: 'auto' });
|
|
503
|
+
this.requestedRate = 1;
|
|
504
|
+
if (this.debugging) console.log('MEWS: returning to realtime delivery');
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
if (buffer < desiredBuffer) {
|
|
508
|
+
// Buffer small enough, return to realtime (mews.js:579-583)
|
|
509
|
+
this.send({ type: 'set_speed', play_rate: 'auto' });
|
|
510
|
+
this.requestedRate = 1;
|
|
511
|
+
if (this.debugging) console.log('MEWS: returning to realtime delivery');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Handle tracks message - codec switch.
|
|
518
|
+
* Ported from mews.js:623-788
|
|
519
|
+
*/
|
|
520
|
+
private handleTracks(msg: MewsMessage): void {
|
|
521
|
+
const codecs: string[] = msg.data?.codecs || [];
|
|
522
|
+
const switchPointMs = msg.data?.current;
|
|
523
|
+
|
|
524
|
+
if (!codecs.length) {
|
|
525
|
+
this.emit('error', 'Track switch contains no codecs');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check if codecs are same as before (mews.js:676)
|
|
530
|
+
const prevCodecs = this.lastCodecs || this.sbManager?.getCodecs() || [];
|
|
531
|
+
if (this.codecsEqual(prevCodecs, codecs)) {
|
|
532
|
+
if (this.debugging) console.log('MEWS: keeping buffer, codecs same');
|
|
533
|
+
// If at position 0 and switch point is not 0, seek to switch point (mews.js:678-679)
|
|
534
|
+
if (this.videoElement?.currentTime === 0 && switchPointMs && switchPointMs !== 0) {
|
|
535
|
+
this.setSeekingPosition(switchPointMs / 1000);
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Different codecs, save for next comparison (mews.js:687)
|
|
541
|
+
this.lastCodecs = codecs;
|
|
542
|
+
|
|
543
|
+
// Change codecs (will handle msgqueue internally)
|
|
544
|
+
this.sbManager?.changeCodecs(codecs, switchPointMs);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Handle pause message.
|
|
549
|
+
* Ported from mews.js:790-792
|
|
550
|
+
*/
|
|
551
|
+
private handlePause(): void {
|
|
552
|
+
if (this.sbManager) {
|
|
553
|
+
this.sbManager.paused = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Set video currentTime with retry logic.
|
|
559
|
+
* Ported from mews.js:635-672
|
|
560
|
+
*/
|
|
561
|
+
private setSeekingPosition(tSec: number): void {
|
|
562
|
+
if (!this.videoElement || !this.sbManager) return;
|
|
563
|
+
|
|
564
|
+
const currPos = this.videoElement.currentTime;
|
|
565
|
+
if (currPos > tSec) {
|
|
566
|
+
// Don't seek backwards (mews.js:637-639)
|
|
567
|
+
tSec = currPos;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const buffered = this.videoElement.buffered;
|
|
571
|
+
if (!buffered.length || buffered.end(buffered.length - 1) < tSec) {
|
|
572
|
+
// Desired position not in buffer yet, wait for more data (mews.js:641-644)
|
|
573
|
+
this.sbManager.scheduleAfterUpdate(() => this.setSeekingPosition(tSec));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.videoElement.currentTime = tSec;
|
|
578
|
+
|
|
579
|
+
if (this.videoElement.currentTime < tSec - 0.001) {
|
|
580
|
+
// Didn't reach target, retry (mews.js:648-651)
|
|
581
|
+
this.sbManager.scheduleAfterUpdate(() => this.setSeekingPosition(tSec));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Check if two codec arrays are equivalent (order-independent)
|
|
587
|
+
*/
|
|
588
|
+
private codecsEqual(arr1: string[], arr2: string[]): boolean {
|
|
589
|
+
if (arr1.length !== arr2.length) return false;
|
|
590
|
+
for (const codec of arr1) {
|
|
591
|
+
if (!arr2.includes(codec)) return false;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ========== PUBLIC API ==========
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Play with optional skip to live edge.
|
|
600
|
+
* Ported from mews.js:959-1023
|
|
601
|
+
*/
|
|
602
|
+
async play(): Promise<void> {
|
|
603
|
+
const v = this.videoElement;
|
|
604
|
+
if (!v) return;
|
|
605
|
+
|
|
606
|
+
// If already playing, nothing to do (mews.js:961-964)
|
|
607
|
+
if (!v.paused) return;
|
|
608
|
+
|
|
609
|
+
// Wait for ready state (codec_data received) with timeout
|
|
610
|
+
if (!this.isReady) {
|
|
611
|
+
await new Promise<void>((resolve, reject) => {
|
|
612
|
+
const timeout = setTimeout(() => {
|
|
613
|
+
reject(new Error('MEWS: Timeout waiting for codec data'));
|
|
614
|
+
}, 5000);
|
|
615
|
+
this.readyResolvers.push(() => {
|
|
616
|
+
clearTimeout(timeout);
|
|
617
|
+
resolve();
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Use listener to wait for on_time before playing (mews.js:973-1017)
|
|
623
|
+
return new Promise((resolve, reject) => {
|
|
624
|
+
// Flag to prevent race condition where multiple on_time messages
|
|
625
|
+
// could trigger seek before the first completes
|
|
626
|
+
let handled = false;
|
|
627
|
+
|
|
628
|
+
const onTime: MewsMessageListener = (msg) => {
|
|
629
|
+
// Remove listener immediately to prevent race condition (single-use pattern)
|
|
630
|
+
if (handled) return;
|
|
631
|
+
handled = true;
|
|
632
|
+
this.wsManager?.removeListener('on_time', onTime);
|
|
633
|
+
|
|
634
|
+
if (!this.sbManager) {
|
|
635
|
+
if (this.debugging) console.log('MEWS: play waiting for sourceBuffer');
|
|
636
|
+
handled = false; // Allow retry
|
|
637
|
+
this.wsManager?.addListener('on_time', onTime);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const data = (msg as OnTimeMessage).data;
|
|
642
|
+
|
|
643
|
+
if (this.streamType === 'live') {
|
|
644
|
+
// Live stream - wait for buffer then seek to live edge (mews.js:978-998)
|
|
645
|
+
const waitForBuffer = () => {
|
|
646
|
+
if (!v.buffered.length) return;
|
|
647
|
+
|
|
648
|
+
const bufferIdx = this.sbManager?.findBufferIndex(data.current / 1000);
|
|
649
|
+
if (typeof bufferIdx === 'number') {
|
|
650
|
+
// Check if current position is in buffer
|
|
651
|
+
if (v.buffered.start(bufferIdx) > v.currentTime || v.buffered.end(bufferIdx) < v.currentTime) {
|
|
652
|
+
v.currentTime = data.current / 1000;
|
|
653
|
+
if (this.debugging) console.log('MEWS: seeking to live position', v.currentTime);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
v.play().then(resolve).catch((err) => {
|
|
657
|
+
this.pause();
|
|
658
|
+
reject(err);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
this.sbManager!.paused = false;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// Wait for buffer via updateend
|
|
666
|
+
this.sbManager?.scheduleAfterUpdate(waitForBuffer);
|
|
667
|
+
} else {
|
|
668
|
+
// VoD - just play when we have data (mews.js:1010-1016)
|
|
669
|
+
this.sbManager!.paused = false;
|
|
670
|
+
if (v.buffered.length && v.buffered.start(0) > v.currentTime) {
|
|
671
|
+
v.currentTime = v.buffered.start(0);
|
|
672
|
+
}
|
|
673
|
+
v.play().then(resolve).catch(reject);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
this.wsManager?.addListener('on_time', onTime);
|
|
678
|
+
|
|
679
|
+
// Send play command (mews.js:1020-1022)
|
|
680
|
+
const skipToLive = this.streamType === 'live' && v.currentTime === 0;
|
|
681
|
+
if (skipToLive) {
|
|
682
|
+
this.send({ type: 'play', seek_time: 'live' });
|
|
683
|
+
} else {
|
|
684
|
+
this.send({ type: 'play' });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Pause playback and server delivery.
|
|
691
|
+
* Ported from mews.js:1025-1029
|
|
692
|
+
*/
|
|
693
|
+
pause(): void {
|
|
694
|
+
this.videoElement?.pause();
|
|
695
|
+
this.send({ type: 'hold' });
|
|
696
|
+
if (this.sbManager) {
|
|
697
|
+
this.sbManager.paused = true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Seek to position with server sync.
|
|
703
|
+
* Ported from mews.js:1071-1111
|
|
704
|
+
*/
|
|
705
|
+
seek(time: number): void {
|
|
706
|
+
if (!this.videoElement || isNaN(time) || time < 0) return;
|
|
707
|
+
|
|
708
|
+
// Calculate seek time with server delay compensation (mews.js:1082)
|
|
709
|
+
const seekMs = Math.round(Math.max(0, time * 1000 - (250 + this.getServerDelay())));
|
|
710
|
+
|
|
711
|
+
this.logDelay('seek');
|
|
712
|
+
this.send({ type: 'seek', seek_time: seekMs });
|
|
713
|
+
|
|
714
|
+
// Wait for seek acknowledgment then on_time (mews.js:1084-1108)
|
|
715
|
+
const onSeek: MewsMessageListener = () => {
|
|
716
|
+
this.wsManager?.removeListener('seek', onSeek);
|
|
717
|
+
|
|
718
|
+
const onTime: MewsMessageListener = (msg) => {
|
|
719
|
+
this.wsManager?.removeListener('on_time', onTime);
|
|
720
|
+
|
|
721
|
+
// Use server's actual position (mews.js:1089)
|
|
722
|
+
const actualTime = (msg as OnTimeMessage).data.current / 1000;
|
|
723
|
+
this.trySetCurrentTime(actualTime);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
this.wsManager?.addListener('on_time', onTime);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
this.wsManager?.addListener('seek', onSeek);
|
|
730
|
+
|
|
731
|
+
// Also set directly as fallback
|
|
732
|
+
this.videoElement.currentTime = time;
|
|
733
|
+
if (this.debugging) console.log('MEWS: seeking to', time);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Try to set currentTime with retry logic.
|
|
738
|
+
* Ported from mews.js:1092-1103
|
|
739
|
+
*/
|
|
740
|
+
private trySetCurrentTime(tSec: number, retries = 10): void {
|
|
741
|
+
const v = this.videoElement;
|
|
742
|
+
if (!v) return;
|
|
743
|
+
|
|
744
|
+
v.currentTime = tSec;
|
|
745
|
+
|
|
746
|
+
if (v.currentTime < tSec - 0.001 && retries > 0) {
|
|
747
|
+
// Failed to seek, retry (mews.js:1095-1100)
|
|
748
|
+
this.sbManager?.scheduleAfterUpdate(() => this.trySetCurrentTime(tSec, retries - 1));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
getCurrentTime(): number {
|
|
753
|
+
return this.videoElement?.currentTime ?? 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
getDuration(): number {
|
|
757
|
+
return isFinite(this.lastDuration) ? this.lastDuration : super.getDuration();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Set playback rate.
|
|
762
|
+
* Ported from mews.js:1119-1129
|
|
763
|
+
*/
|
|
764
|
+
setPlaybackRate(rate: number): void {
|
|
765
|
+
super.setPlaybackRate(rate);
|
|
766
|
+
const playRate = rate === 1 ? 'auto' : rate;
|
|
767
|
+
this.logDelay('set_speed');
|
|
768
|
+
this.send({ type: 'set_speed', play_rate: playRate });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
getQualities(): Array<{ id: string; label: string; isAuto?: boolean; active?: boolean }> {
|
|
772
|
+
return [{ id: 'auto', label: 'Auto', isAuto: true, active: true }];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
selectQuality(id: string): void {
|
|
776
|
+
if (id === 'auto') {
|
|
777
|
+
this.send({ type: 'set_speed', play_rate: 'auto' });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Set tracks for ABR or quality selection.
|
|
783
|
+
* Ported from mews.js:1030-1037
|
|
784
|
+
*/
|
|
785
|
+
setTracks(obj: { video?: string; audio?: string; subtitle?: string }): void {
|
|
786
|
+
if (!Object.keys(obj).length) return;
|
|
787
|
+
this.send({ type: 'tracks', ...obj });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Select a subtitle track.
|
|
792
|
+
*/
|
|
793
|
+
selectTextTrack(id: string | null): void {
|
|
794
|
+
if (id === null) {
|
|
795
|
+
this.send({ type: 'tracks', subtitle: 'none' });
|
|
796
|
+
} else {
|
|
797
|
+
this.send({ type: 'tracks', subtitle: id });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
isLive(): boolean {
|
|
802
|
+
return this.streamType === 'live';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Jump to live edge.
|
|
807
|
+
*/
|
|
808
|
+
jumpToLive(): void {
|
|
809
|
+
if (this.streamType !== 'live' || !this.wsManager) return;
|
|
810
|
+
this.send({ type: 'play', seek_time: 'live' });
|
|
811
|
+
this.videoElement?.play().catch(() => {});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async getStats(): Promise<any> {
|
|
815
|
+
return {
|
|
816
|
+
currentBps: this.currentBps,
|
|
817
|
+
waitingEvents: this.nWaiting,
|
|
818
|
+
isLive: this.streamType === 'live',
|
|
819
|
+
serverDelay: this.getServerDelay()
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ========== EVENT HANDLERS ==========
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Install waiting event handler.
|
|
827
|
+
* Handles buffer gaps and ABR.
|
|
828
|
+
* Ported from mews.js:1177-1186, 1272-1278
|
|
829
|
+
*/
|
|
830
|
+
private installWaitingHandler(): void {
|
|
831
|
+
if (!this.videoElement) return;
|
|
832
|
+
|
|
833
|
+
this.videoElement.addEventListener('waiting', () => {
|
|
834
|
+
if (this.seeking) return;
|
|
835
|
+
|
|
836
|
+
const v = this.videoElement!;
|
|
837
|
+
if (!v.buffered || !v.buffered.length) return;
|
|
838
|
+
|
|
839
|
+
// Check for buffer gap and jump it (mews.js:1180-1186)
|
|
840
|
+
const bufferIdx = this.sbManager?.findBufferIndex(v.currentTime);
|
|
841
|
+
if (bufferIdx !== false && typeof bufferIdx === 'number') {
|
|
842
|
+
if (bufferIdx + 1 < v.buffered.length) {
|
|
843
|
+
const nextStart = v.buffered.start(bufferIdx + 1);
|
|
844
|
+
if (nextStart - v.currentTime < 10) {
|
|
845
|
+
if (this.debugging) console.log('MEWS: skipping buffer gap to', nextStart);
|
|
846
|
+
v.currentTime = nextStart;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ABR trigger (mews.js:1272-1278)
|
|
852
|
+
this.nWaiting++;
|
|
853
|
+
if (this.nWaiting >= this.nWaitingThreshold && this.currentBps) {
|
|
854
|
+
this.nWaiting = 0;
|
|
855
|
+
if (this.debugging) console.log('MEWS: ABR triggered, requesting lower bitrate');
|
|
856
|
+
this.setTracks({ video: `<${Math.round(this.currentBps)}bps,minbps` });
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Install seeking event handlers.
|
|
863
|
+
* Ported from mews.js:1169-1175
|
|
864
|
+
*/
|
|
865
|
+
private installSeekingHandler(): void {
|
|
866
|
+
if (!this.videoElement) return;
|
|
867
|
+
|
|
868
|
+
this.videoElement.addEventListener('seeking', () => {
|
|
869
|
+
this.seeking = true;
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
this.videoElement.addEventListener('seeked', () => {
|
|
873
|
+
this.seeking = false;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Install pause event handler for browser pause detection.
|
|
879
|
+
* Ported from mews.js:1188-1200
|
|
880
|
+
*/
|
|
881
|
+
private installPauseHandler(): void {
|
|
882
|
+
if (!this.videoElement) return;
|
|
883
|
+
|
|
884
|
+
this.videoElement.addEventListener('pause', () => {
|
|
885
|
+
if (this.sbManager && !this.sbManager.paused) {
|
|
886
|
+
// Browser paused (probably tab hidden) - pause download (mews.js:1189-1192)
|
|
887
|
+
if (this.debugging) console.log('MEWS: browser paused, pausing download');
|
|
888
|
+
this.send({ type: 'hold' });
|
|
889
|
+
this.sbManager.paused = true;
|
|
890
|
+
|
|
891
|
+
// Resume on play (mews.js:1193-1197)
|
|
892
|
+
const onPlay = () => {
|
|
893
|
+
if (this.sbManager?.paused) {
|
|
894
|
+
this.send({ type: 'play' });
|
|
895
|
+
}
|
|
896
|
+
this.videoElement?.removeEventListener('play', onPlay);
|
|
897
|
+
};
|
|
898
|
+
this.videoElement?.addEventListener('play', onPlay);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Install loop handler for VoD content.
|
|
905
|
+
* Ported from mews.js:1157-1167
|
|
906
|
+
*/
|
|
907
|
+
private installLoopHandler(): void {
|
|
908
|
+
if (!this.videoElement) return;
|
|
909
|
+
|
|
910
|
+
this.videoElement.addEventListener('ended', () => {
|
|
911
|
+
const v = this.videoElement;
|
|
912
|
+
if (!v) return;
|
|
913
|
+
|
|
914
|
+
if (v.loop && this.streamType !== 'live') {
|
|
915
|
+
// Loop VoD content (mews.js:1159-1166)
|
|
916
|
+
this.seek(0);
|
|
917
|
+
this.sbManager?._do(() => {
|
|
918
|
+
try {
|
|
919
|
+
// Clear buffer for clean loop
|
|
920
|
+
} catch (e) {}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ========== UTILITIES ==========
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Send command to server with retry.
|
|
930
|
+
* Ported from mews.js:904-944
|
|
931
|
+
*/
|
|
932
|
+
private send(cmd: object): void {
|
|
933
|
+
if (this.wsManager) {
|
|
934
|
+
this.wsManager.send(cmd);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Log delay for server RTT estimation.
|
|
940
|
+
* Ported from mews.js:835-862
|
|
941
|
+
*/
|
|
942
|
+
private logDelay(type: string): void {
|
|
943
|
+
this.pendingDelayTypes[type] = Date.now();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Resolve delay measurement.
|
|
948
|
+
* Ported from mews.js:855-861, 863-867
|
|
949
|
+
*/
|
|
950
|
+
private resolveDelay(type: string): void {
|
|
951
|
+
const start = this.pendingDelayTypes[type];
|
|
952
|
+
if (start) {
|
|
953
|
+
const delay = Date.now() - start;
|
|
954
|
+
this.serverDelays.unshift(delay);
|
|
955
|
+
if (this.serverDelays.length > 5) {
|
|
956
|
+
this.serverDelays.pop();
|
|
957
|
+
}
|
|
958
|
+
delete this.pendingDelayTypes[type];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Get average server delay.
|
|
964
|
+
* Ported from mews.js:869-881
|
|
965
|
+
*/
|
|
966
|
+
private getServerDelay(): number {
|
|
967
|
+
if (!this.serverDelays.length) return 500;
|
|
968
|
+
const n = Math.min(3, this.serverDelays.length);
|
|
969
|
+
let sum = 0;
|
|
970
|
+
for (let i = 0; i < n; i++) {
|
|
971
|
+
sum += this.serverDelays[i];
|
|
972
|
+
}
|
|
973
|
+
return sum / n;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Track bandwidth for ABR.
|
|
978
|
+
* Ported from mews.js:1280-1303
|
|
979
|
+
*/
|
|
980
|
+
private trackBits(buf: ArrayBuffer): void {
|
|
981
|
+
this.bitCounter.push(buf.byteLength * 8);
|
|
982
|
+
this.bitsSince.push(Date.now());
|
|
983
|
+
|
|
984
|
+
// Keep window size of 5 samples
|
|
985
|
+
if (this.bitCounter.length > 5) {
|
|
986
|
+
this.bitCounter.shift();
|
|
987
|
+
this.bitsSince.shift();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Calculate current bitrate
|
|
991
|
+
if (this.bitCounter.length >= 2) {
|
|
992
|
+
const bits = this.bitCounter[this.bitCounter.length - 1];
|
|
993
|
+
const dt = (this.bitsSince[this.bitsSince.length - 1] - this.bitsSince[0]) / 1000;
|
|
994
|
+
if (dt > 0) {
|
|
995
|
+
this.currentBps = Math.round(bits / dt);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private startTelemetry(): void {
|
|
1001
|
+
if (!this.analyticsConfig.enabled || !this.analyticsConfig.endpoint) return;
|
|
1002
|
+
|
|
1003
|
+
const endpoint = this.analyticsConfig.endpoint;
|
|
1004
|
+
|
|
1005
|
+
this.analyticsTimer = setInterval(async () => {
|
|
1006
|
+
if (!this.videoElement) return;
|
|
1007
|
+
|
|
1008
|
+
const stats = await this.getStats();
|
|
1009
|
+
const payload = {
|
|
1010
|
+
t: Date.now(),
|
|
1011
|
+
bps: stats.currentBps || 0,
|
|
1012
|
+
waiting: stats.waitingEvents || 0
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
await fetch(endpoint, {
|
|
1017
|
+
method: 'POST',
|
|
1018
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1019
|
+
body: JSON.stringify(payload)
|
|
1020
|
+
});
|
|
1021
|
+
} catch {}
|
|
1022
|
+
}, 5000);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async destroy(): Promise<void> {
|
|
1026
|
+
console.debug('[MEWS] destroy() called');
|
|
1027
|
+
this.isDestroyed = true;
|
|
1028
|
+
this.isReady = false;
|
|
1029
|
+
this.readyResolvers = [];
|
|
1030
|
+
|
|
1031
|
+
if (this.analyticsTimer) {
|
|
1032
|
+
clearInterval(this.analyticsTimer);
|
|
1033
|
+
this.analyticsTimer = null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// CRITICAL: Close WebSocket FIRST to stop all network activity immediately
|
|
1037
|
+
// Don't send 'stop' message - it can trigger retry logic and delay cleanup
|
|
1038
|
+
this.wsManager?.destroy();
|
|
1039
|
+
this.wsManager = null;
|
|
1040
|
+
|
|
1041
|
+
this.sbManager?.destroy();
|
|
1042
|
+
this.sbManager = null;
|
|
1043
|
+
|
|
1044
|
+
if (this.mediaSource?.readyState === 'open') {
|
|
1045
|
+
try {
|
|
1046
|
+
this.mediaSource.endOfStream();
|
|
1047
|
+
} catch {}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (this.objectUrl) {
|
|
1051
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
1052
|
+
this.objectUrl = null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (this.videoElement && this.container) {
|
|
1056
|
+
try { this.container.removeChild(this.videoElement); } catch {}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
this.videoElement = null;
|
|
1060
|
+
this.container = null;
|
|
1061
|
+
this.mediaSource = null;
|
|
1062
|
+
this.listeners.clear();
|
|
1063
|
+
console.debug('[MEWS] destroy() completed');
|
|
1064
|
+
}
|
|
1065
|
+
}
|