@livepeer-frameworks/player-core 0.1.0 → 0.1.1
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/README.md +11 -9
- package/dist/player.css +182 -42
- package/package.json +1 -1
- package/src/core/ABRController.ts +38 -36
- package/src/core/CodecUtils.ts +49 -46
- package/src/core/Disposable.ts +4 -4
- package/src/core/EventEmitter.ts +1 -1
- package/src/core/GatewayClient.ts +41 -39
- package/src/core/InteractionController.ts +89 -82
- package/src/core/LiveDurationProxy.ts +14 -15
- package/src/core/MetaTrackManager.ts +73 -65
- package/src/core/MistReporter.ts +72 -45
- package/src/core/MistSignaling.ts +59 -56
- package/src/core/PlayerController.ts +527 -384
- package/src/core/PlayerInterface.ts +83 -59
- package/src/core/PlayerManager.ts +79 -133
- package/src/core/PlayerRegistry.ts +59 -42
- package/src/core/QualityMonitor.ts +38 -31
- package/src/core/ScreenWakeLockManager.ts +8 -9
- package/src/core/SeekingUtils.ts +31 -22
- package/src/core/StreamStateClient.ts +74 -68
- package/src/core/SubtitleManager.ts +24 -22
- package/src/core/TelemetryReporter.ts +34 -31
- package/src/core/TimeFormat.ts +13 -17
- package/src/core/TimerManager.ts +24 -8
- package/src/core/UrlUtils.ts +20 -17
- package/src/core/detector.ts +44 -44
- package/src/core/index.ts +57 -48
- package/src/core/scorer.ts +136 -141
- package/src/core/selector.ts +2 -6
- package/src/global.d.ts +1 -1
- package/src/index.ts +46 -35
- package/src/players/DashJsPlayer.ts +164 -115
- package/src/players/HlsJsPlayer.ts +132 -78
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +41 -36
- package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -9
- package/src/players/MewsWsPlayer/index.ts +192 -152
- package/src/players/MewsWsPlayer/types.ts +21 -21
- package/src/players/MistPlayer.ts +45 -26
- package/src/players/MistWebRTCPlayer/index.ts +175 -129
- package/src/players/NativePlayer.ts +203 -143
- package/src/players/VideoJsPlayer.ts +170 -118
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
- package/src/players/WebCodecsPlayer/SyncController.ts +45 -53
- package/src/players/WebCodecsPlayer/WebSocketController.ts +66 -68
- package/src/players/WebCodecsPlayer/index.ts +263 -221
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
- package/src/players/WebCodecsPlayer/types.ts +56 -56
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +238 -182
- package/src/players/WebCodecsPlayer/worker/types.ts +31 -31
- package/src/players/index.ts +8 -8
- package/src/styles/animations.css +2 -1
- package/src/styles/player.css +182 -42
- package/src/styles/tailwind.css +473 -159
- package/src/types.ts +43 -43
- package/src/vanilla/FrameWorksPlayer.ts +29 -14
- package/src/vanilla/index.ts +4 -4
|
@@ -10,17 +10,22 @@
|
|
|
10
10
|
* - DataChannel for timed metadata
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { BasePlayer } from
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
import { BasePlayer } from "../../core/PlayerInterface";
|
|
14
|
+
import type {
|
|
15
|
+
StreamSource,
|
|
16
|
+
StreamInfo,
|
|
17
|
+
PlayerOptions,
|
|
18
|
+
PlayerCapability,
|
|
19
|
+
} from "../../core/PlayerInterface";
|
|
20
|
+
import { MistSignaling, type MistTimeUpdate } from "../../core/MistSignaling";
|
|
21
|
+
import { checkWebRTCCodecCompatibility } from "../../core/detector";
|
|
17
22
|
|
|
18
23
|
export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
19
24
|
readonly capability: PlayerCapability = {
|
|
20
25
|
name: "MistServer WebRTC",
|
|
21
26
|
shortname: "mist-webrtc",
|
|
22
27
|
priority: 2, // After direct (WHEP=1), before HLS.js (3)
|
|
23
|
-
mimes: ["webrtc", "mist/webrtc"]
|
|
28
|
+
mimes: ["webrtc", "mist/webrtc"],
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
private signaling: MistSignaling | null = null;
|
|
@@ -33,7 +38,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
33
38
|
private seekOffset = 0;
|
|
34
39
|
private durationMs = 0;
|
|
35
40
|
private isLiveStream = true;
|
|
36
|
-
private playRate: number |
|
|
41
|
+
private playRate: number | "auto" = "auto";
|
|
37
42
|
|
|
38
43
|
// Buffer window tracking (P2)
|
|
39
44
|
private bufferWindow = 0;
|
|
@@ -46,7 +51,11 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
46
51
|
private currentOptions: PlayerOptions | null = null;
|
|
47
52
|
|
|
48
53
|
// Stats tracking
|
|
49
|
-
private lastInboundStats: {
|
|
54
|
+
private lastInboundStats: {
|
|
55
|
+
video?: { bytesReceived: number };
|
|
56
|
+
audio?: { bytesReceived: number };
|
|
57
|
+
timestamp: number;
|
|
58
|
+
} | null = null;
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* Chrome on Android has a bug where H264 is not available immediately
|
|
@@ -56,16 +65,16 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
56
65
|
private async checkH264Available(retries = 5): Promise<boolean> {
|
|
57
66
|
for (let i = 0; i < retries; i++) {
|
|
58
67
|
try {
|
|
59
|
-
const caps = RTCRtpReceiver.getCapabilities?.(
|
|
60
|
-
if (caps?.codecs.some(c => c.mimeType ===
|
|
68
|
+
const caps = RTCRtpReceiver.getCapabilities?.("video");
|
|
69
|
+
if (caps?.codecs.some((c) => c.mimeType === "video/H264")) {
|
|
61
70
|
return true;
|
|
62
71
|
}
|
|
63
72
|
} catch {}
|
|
64
73
|
if (i < retries - 1) {
|
|
65
|
-
await new Promise(r => setTimeout(r, 100));
|
|
74
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
|
-
console.warn(
|
|
77
|
+
console.warn("[MistWebRTC] H264 not available after retries");
|
|
69
78
|
return false;
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -77,14 +86,14 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
77
86
|
if ((window as any).WebRTCBrowserEqualizerLoaded) return;
|
|
78
87
|
|
|
79
88
|
return new Promise((resolve) => {
|
|
80
|
-
const script = document.createElement(
|
|
89
|
+
const script = document.createElement("script");
|
|
81
90
|
script.src = `${host}/webrtc.js`;
|
|
82
91
|
script.onload = () => {
|
|
83
|
-
console.debug(
|
|
92
|
+
console.debug("[MistWebRTC] Browser equalizer loaded");
|
|
84
93
|
resolve();
|
|
85
94
|
};
|
|
86
95
|
script.onerror = () => {
|
|
87
|
-
console.warn(
|
|
96
|
+
console.warn("[MistWebRTC] Failed to load browser equalizer");
|
|
88
97
|
resolve(); // Non-fatal
|
|
89
98
|
};
|
|
90
99
|
document.head.appendChild(script);
|
|
@@ -105,35 +114,46 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
105
114
|
return this.capability.mimes.includes(mimetype);
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
isBrowserSupported(
|
|
117
|
+
isBrowserSupported(
|
|
118
|
+
mimetype: string,
|
|
119
|
+
source: StreamSource,
|
|
120
|
+
streamInfo: StreamInfo
|
|
121
|
+
): boolean | string[] {
|
|
109
122
|
// Check basic WebRTC support
|
|
110
|
-
if (!(
|
|
123
|
+
if (!("RTCPeerConnection" in window) || !("WebSocket" in window)) return false;
|
|
111
124
|
|
|
112
125
|
// Check codec compatibility
|
|
113
126
|
const codecCompat = checkWebRTCCodecCompatibility(streamInfo.meta.tracks);
|
|
114
127
|
if (!codecCompat.compatible) {
|
|
115
|
-
console.debug(
|
|
128
|
+
console.debug(
|
|
129
|
+
"[MistWebRTC] Skipping - incompatible codecs:",
|
|
130
|
+
codecCompat.incompatibleCodecs.join(", ")
|
|
131
|
+
);
|
|
116
132
|
return false;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
// Return which track types we can play
|
|
120
136
|
const playable: string[] = [];
|
|
121
137
|
if (codecCompat.details.compatibleVideoCodecs.length > 0) {
|
|
122
|
-
playable.push(
|
|
138
|
+
playable.push("video");
|
|
123
139
|
}
|
|
124
140
|
if (codecCompat.details.compatibleAudioCodecs.length > 0) {
|
|
125
|
-
playable.push(
|
|
141
|
+
playable.push("audio");
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
return playable.length > 0 ? playable : false;
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
async initialize(
|
|
147
|
+
async initialize(
|
|
148
|
+
container: HTMLElement,
|
|
149
|
+
source: StreamSource,
|
|
150
|
+
options: PlayerOptions
|
|
151
|
+
): Promise<HTMLVideoElement> {
|
|
132
152
|
this.destroyed = false;
|
|
133
153
|
this.container = container;
|
|
134
154
|
this.currentSource = source;
|
|
135
155
|
this.currentOptions = options;
|
|
136
|
-
container.classList.add(
|
|
156
|
+
container.classList.add("fw-player-container");
|
|
137
157
|
|
|
138
158
|
// Load browser equalizer script (P0) - extract host from source URL
|
|
139
159
|
try {
|
|
@@ -146,10 +166,10 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
146
166
|
await this.checkH264Available();
|
|
147
167
|
|
|
148
168
|
// Create video element
|
|
149
|
-
const video = document.createElement(
|
|
150
|
-
video.classList.add(
|
|
151
|
-
video.setAttribute(
|
|
152
|
-
video.setAttribute(
|
|
169
|
+
const video = document.createElement("video");
|
|
170
|
+
video.classList.add("fw-player-video");
|
|
171
|
+
video.setAttribute("playsinline", "");
|
|
172
|
+
video.setAttribute("crossorigin", "anonymous");
|
|
153
173
|
|
|
154
174
|
if (options.autoplay) video.autoplay = true;
|
|
155
175
|
if (options.muted) video.muted = true;
|
|
@@ -165,7 +185,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
165
185
|
await this.setupWebRTC(video, source, options);
|
|
166
186
|
return video;
|
|
167
187
|
} catch (error: any) {
|
|
168
|
-
this.emit(
|
|
188
|
+
this.emit("error", error.message || String(error));
|
|
169
189
|
throw error;
|
|
170
190
|
}
|
|
171
191
|
}
|
|
@@ -184,23 +204,31 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
184
204
|
|
|
185
205
|
// Close data channel
|
|
186
206
|
if (this.dataChannel) {
|
|
187
|
-
try {
|
|
207
|
+
try {
|
|
208
|
+
this.dataChannel.close();
|
|
209
|
+
} catch {}
|
|
188
210
|
this.dataChannel = null;
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
// Close peer connection
|
|
192
214
|
if (this.peerConnection) {
|
|
193
|
-
try {
|
|
215
|
+
try {
|
|
216
|
+
this.peerConnection.close();
|
|
217
|
+
} catch {}
|
|
194
218
|
this.peerConnection = null;
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
// Clean up video element
|
|
198
222
|
if (this.videoElement) {
|
|
199
|
-
try {
|
|
223
|
+
try {
|
|
224
|
+
(this.videoElement as any).srcObject = null;
|
|
225
|
+
} catch {}
|
|
200
226
|
this.videoElement.pause();
|
|
201
227
|
|
|
202
228
|
if (this.container) {
|
|
203
|
-
try {
|
|
229
|
+
try {
|
|
230
|
+
this.container.removeChild(this.videoElement);
|
|
231
|
+
} catch {}
|
|
204
232
|
}
|
|
205
233
|
}
|
|
206
234
|
|
|
@@ -216,7 +244,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
216
244
|
this.videoElement.pause();
|
|
217
245
|
this.seekOffset = time - this.videoElement.currentTime;
|
|
218
246
|
this.signaling.seek(time).catch((e) => {
|
|
219
|
-
console.warn(
|
|
247
|
+
console.warn("[MistWebRTC] Seek failed:", e);
|
|
220
248
|
});
|
|
221
249
|
}
|
|
222
250
|
|
|
@@ -232,8 +260,8 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
232
260
|
|
|
233
261
|
this.videoElement.pause();
|
|
234
262
|
this.seekOffset = 0;
|
|
235
|
-
this.signaling.seek(
|
|
236
|
-
console.warn(
|
|
263
|
+
this.signaling.seek("live").catch((e) => {
|
|
264
|
+
console.warn("[MistWebRTC] Jump to live failed:", e);
|
|
237
265
|
});
|
|
238
266
|
}
|
|
239
267
|
|
|
@@ -260,7 +288,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
260
288
|
getQualities(): Array<{ id: string; label: string; isAuto?: boolean; active?: boolean }> {
|
|
261
289
|
// Always offer auto as first option
|
|
262
290
|
const qualities: Array<{ id: string; label: string; isAuto?: boolean; active?: boolean }> = [
|
|
263
|
-
{ id:
|
|
291
|
+
{ id: "auto", label: "Auto", isAuto: true, active: this.playRate === "auto" },
|
|
264
292
|
];
|
|
265
293
|
|
|
266
294
|
// If we have track info from signaling, add quality options
|
|
@@ -274,8 +302,8 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
274
302
|
selectQuality(id: string): void {
|
|
275
303
|
if (!this.signaling?.isConnected) return;
|
|
276
304
|
|
|
277
|
-
if (id ===
|
|
278
|
-
this.signaling.setSpeed(
|
|
305
|
+
if (id === "auto") {
|
|
306
|
+
this.signaling.setSpeed("auto");
|
|
279
307
|
} else {
|
|
280
308
|
// Track selection: ~widthxheight or |bitrate
|
|
281
309
|
this.signaling.setTracks({ video: id });
|
|
@@ -287,63 +315,67 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
287
315
|
if (!this.signaling?.isConnected) return;
|
|
288
316
|
|
|
289
317
|
if (id === null) {
|
|
290
|
-
this.signaling.setTracks({ video:
|
|
318
|
+
this.signaling.setTracks({ video: "none" });
|
|
291
319
|
} else {
|
|
292
320
|
this.signaling.setTracks({ video: id });
|
|
293
321
|
}
|
|
294
322
|
}
|
|
295
323
|
|
|
296
|
-
async getStats(): Promise<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
324
|
+
async getStats(): Promise<
|
|
325
|
+
| {
|
|
326
|
+
type: "webrtc";
|
|
327
|
+
video?: {
|
|
328
|
+
bytesReceived: number;
|
|
329
|
+
packetsReceived: number;
|
|
330
|
+
packetsLost: number;
|
|
331
|
+
packetLossRate: number;
|
|
332
|
+
jitter: number;
|
|
333
|
+
framesDecoded: number;
|
|
334
|
+
framesDropped: number;
|
|
335
|
+
frameDropRate: number;
|
|
336
|
+
frameWidth: number;
|
|
337
|
+
frameHeight: number;
|
|
338
|
+
framesPerSecond: number;
|
|
339
|
+
bitrate: number;
|
|
340
|
+
jitterBufferDelay: number;
|
|
341
|
+
};
|
|
342
|
+
audio?: {
|
|
343
|
+
bytesReceived: number;
|
|
344
|
+
packetsReceived: number;
|
|
345
|
+
packetsLost: number;
|
|
346
|
+
packetLossRate: number;
|
|
347
|
+
jitter: number;
|
|
348
|
+
bitrate: number;
|
|
349
|
+
};
|
|
350
|
+
network?: {
|
|
351
|
+
rtt: number;
|
|
352
|
+
availableOutgoingBitrate: number;
|
|
353
|
+
availableIncomingBitrate: number;
|
|
354
|
+
bytesSent: number;
|
|
355
|
+
bytesReceived: number;
|
|
356
|
+
};
|
|
357
|
+
timestamp: number;
|
|
358
|
+
}
|
|
359
|
+
| undefined
|
|
360
|
+
> {
|
|
330
361
|
if (!this.peerConnection) return undefined;
|
|
331
362
|
|
|
332
363
|
try {
|
|
333
364
|
const stats = await this.peerConnection.getStats();
|
|
334
365
|
const now = Date.now();
|
|
335
|
-
const result: any = { type:
|
|
366
|
+
const result: any = { type: "webrtc", timestamp: now };
|
|
336
367
|
|
|
337
368
|
stats.forEach((report: any) => {
|
|
338
|
-
if (report.type ===
|
|
339
|
-
const packetLossRate =
|
|
340
|
-
|
|
341
|
-
|
|
369
|
+
if (report.type === "inbound-rtp") {
|
|
370
|
+
const packetLossRate =
|
|
371
|
+
report.packetsReceived > 0
|
|
372
|
+
? (report.packetsLost / (report.packetsReceived + report.packetsLost)) * 100
|
|
373
|
+
: 0;
|
|
342
374
|
|
|
343
375
|
// Calculate bitrate from previous sample
|
|
344
376
|
let bitrate = 0;
|
|
345
|
-
if (this.lastInboundStats && this.lastInboundStats[report.kind as
|
|
346
|
-
const prev = this.lastInboundStats[report.kind as
|
|
377
|
+
if (this.lastInboundStats && this.lastInboundStats[report.kind as "video" | "audio"]) {
|
|
378
|
+
const prev = this.lastInboundStats[report.kind as "video" | "audio"];
|
|
347
379
|
const timeDelta = (now - this.lastInboundStats.timestamp) / 1000;
|
|
348
380
|
if (timeDelta > 0 && prev) {
|
|
349
381
|
const bytesDelta = report.bytesReceived - prev.bytesReceived;
|
|
@@ -351,10 +383,11 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
351
383
|
}
|
|
352
384
|
}
|
|
353
385
|
|
|
354
|
-
if (report.kind ===
|
|
355
|
-
const frameDropRate =
|
|
356
|
-
|
|
357
|
-
|
|
386
|
+
if (report.kind === "video") {
|
|
387
|
+
const frameDropRate =
|
|
388
|
+
report.framesDecoded > 0
|
|
389
|
+
? (report.framesDropped / (report.framesDecoded + report.framesDropped)) * 100
|
|
390
|
+
: 0;
|
|
358
391
|
|
|
359
392
|
result.video = {
|
|
360
393
|
bytesReceived: report.bytesReceived || 0,
|
|
@@ -369,12 +402,13 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
369
402
|
frameHeight: report.frameHeight || 0,
|
|
370
403
|
framesPerSecond: report.framesPerSecond || 0,
|
|
371
404
|
bitrate,
|
|
372
|
-
jitterBufferDelay:
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
jitterBufferDelay:
|
|
406
|
+
report.jitterBufferDelay && report.jitterBufferEmittedCount
|
|
407
|
+
? (report.jitterBufferDelay / report.jitterBufferEmittedCount) * 1000
|
|
408
|
+
: 0,
|
|
375
409
|
};
|
|
376
410
|
}
|
|
377
|
-
if (report.kind ===
|
|
411
|
+
if (report.kind === "audio") {
|
|
378
412
|
result.audio = {
|
|
379
413
|
bytesReceived: report.bytesReceived || 0,
|
|
380
414
|
packetsReceived: report.packetsReceived || 0,
|
|
@@ -385,7 +419,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
385
419
|
};
|
|
386
420
|
}
|
|
387
421
|
}
|
|
388
|
-
if (report.type ===
|
|
422
|
+
if (report.type === "candidate-pair" && report.nominated) {
|
|
389
423
|
result.network = {
|
|
390
424
|
rtt: report.currentRoundTripTime ? report.currentRoundTripTime * 1000 : 0,
|
|
391
425
|
availableOutgoingBitrate: report.availableOutgoingBitrate || 0,
|
|
@@ -409,7 +443,9 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
409
443
|
}
|
|
410
444
|
}
|
|
411
445
|
|
|
412
|
-
async getLatency(): Promise<
|
|
446
|
+
async getLatency(): Promise<
|
|
447
|
+
{ estimatedMs: number; jitterBufferMs: number; rttMs: number } | undefined
|
|
448
|
+
> {
|
|
413
449
|
const s = await this.getStats();
|
|
414
450
|
if (!s) return undefined;
|
|
415
451
|
|
|
@@ -460,15 +496,15 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
460
496
|
const pauseOnFirstPlay = () => {
|
|
461
497
|
video.pause();
|
|
462
498
|
this.signaling?.pause();
|
|
463
|
-
video.removeEventListener(
|
|
499
|
+
video.removeEventListener("play", pauseOnFirstPlay);
|
|
464
500
|
};
|
|
465
|
-
video.addEventListener(
|
|
501
|
+
video.addEventListener("play", pauseOnFirstPlay);
|
|
466
502
|
}
|
|
467
503
|
|
|
468
504
|
// Loop reconnect for VoD content (P1)
|
|
469
|
-
video.addEventListener(
|
|
505
|
+
video.addEventListener("ended", async () => {
|
|
470
506
|
if (video.loop && !this.isLiveStream && this.currentSource && this.currentOptions) {
|
|
471
|
-
console.debug(
|
|
507
|
+
console.debug("[MistWebRTC] VoD ended with loop enabled, reconnecting...");
|
|
472
508
|
try {
|
|
473
509
|
// Partial cleanup - keep container and video element
|
|
474
510
|
if (this.signaling) {
|
|
@@ -479,19 +515,23 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
479
515
|
this.signaling = null;
|
|
480
516
|
}
|
|
481
517
|
if (this.dataChannel) {
|
|
482
|
-
try {
|
|
518
|
+
try {
|
|
519
|
+
this.dataChannel.close();
|
|
520
|
+
} catch {}
|
|
483
521
|
this.dataChannel = null;
|
|
484
522
|
}
|
|
485
523
|
if (this.peerConnection) {
|
|
486
|
-
try {
|
|
524
|
+
try {
|
|
525
|
+
this.peerConnection.close();
|
|
526
|
+
} catch {}
|
|
487
527
|
this.peerConnection = null;
|
|
488
528
|
}
|
|
489
529
|
|
|
490
530
|
// Reconnect WebRTC
|
|
491
531
|
await this.setupWebRTC(video, this.currentSource, this.currentOptions);
|
|
492
532
|
} catch (e) {
|
|
493
|
-
console.error(
|
|
494
|
-
this.emit(
|
|
533
|
+
console.error("[MistWebRTC] Failed to reconnect for loop:", e);
|
|
534
|
+
this.emit("error", "Failed to reconnect for loop");
|
|
495
535
|
}
|
|
496
536
|
}
|
|
497
537
|
});
|
|
@@ -499,7 +539,11 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
499
539
|
|
|
500
540
|
// Private methods
|
|
501
541
|
|
|
502
|
-
private async setupWebRTC(
|
|
542
|
+
private async setupWebRTC(
|
|
543
|
+
video: HTMLVideoElement,
|
|
544
|
+
source: StreamSource,
|
|
545
|
+
_options: PlayerOptions
|
|
546
|
+
): Promise<void> {
|
|
503
547
|
const sourceAny = source as any;
|
|
504
548
|
const iceServers: RTCIceServer[] = sourceAny?.iceServers || [];
|
|
505
549
|
|
|
@@ -515,10 +559,10 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
515
559
|
this.peerConnection = pc;
|
|
516
560
|
|
|
517
561
|
// Create data channel for metadata
|
|
518
|
-
this.dataChannel = pc.createDataChannel(
|
|
562
|
+
this.dataChannel = pc.createDataChannel("*", { protocol: "JSON" });
|
|
519
563
|
this.dataChannel.onmessage = (event) => {
|
|
520
564
|
if (this.destroyed) return;
|
|
521
|
-
console.debug(
|
|
565
|
+
console.debug("[MistWebRTC] DataChannel message:", event.data);
|
|
522
566
|
// Handle timed metadata here if needed
|
|
523
567
|
};
|
|
524
568
|
|
|
@@ -536,8 +580,8 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
536
580
|
const state = pc.connectionState;
|
|
537
581
|
console.debug(`[MistWebRTC] Connection state: ${state}`);
|
|
538
582
|
|
|
539
|
-
if (state ===
|
|
540
|
-
this.emit(
|
|
583
|
+
if (state === "failed") {
|
|
584
|
+
this.emit("error", "WebRTC connection failed (firewall?)");
|
|
541
585
|
}
|
|
542
586
|
};
|
|
543
587
|
|
|
@@ -547,8 +591,8 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
547
591
|
const state = pc.iceConnectionState;
|
|
548
592
|
console.debug(`[MistWebRTC] ICE state: ${state}`);
|
|
549
593
|
|
|
550
|
-
if (state ===
|
|
551
|
-
this.emit(
|
|
594
|
+
if (state === "failed") {
|
|
595
|
+
this.emit("error", "ICE connection failed");
|
|
552
596
|
}
|
|
553
597
|
};
|
|
554
598
|
|
|
@@ -561,15 +605,15 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
561
605
|
// Wait for signaling to connect
|
|
562
606
|
await new Promise<void>((resolve, reject) => {
|
|
563
607
|
const timeout = setTimeout(() => {
|
|
564
|
-
reject(new Error(
|
|
608
|
+
reject(new Error("Signaling connection timeout"));
|
|
565
609
|
}, 10000);
|
|
566
610
|
|
|
567
|
-
this.signaling!.once(
|
|
611
|
+
this.signaling!.once("connected", () => {
|
|
568
612
|
clearTimeout(timeout);
|
|
569
613
|
resolve();
|
|
570
614
|
});
|
|
571
615
|
|
|
572
|
-
this.signaling!.once(
|
|
616
|
+
this.signaling!.once("error", ({ message }) => {
|
|
573
617
|
clearTimeout(timeout);
|
|
574
618
|
reject(new Error(message));
|
|
575
619
|
});
|
|
@@ -581,18 +625,18 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
581
625
|
// Wait for answer
|
|
582
626
|
await new Promise<void>((resolve, reject) => {
|
|
583
627
|
const timeout = setTimeout(() => {
|
|
584
|
-
reject(new Error(
|
|
628
|
+
reject(new Error("SDP answer timeout"));
|
|
585
629
|
}, 10000);
|
|
586
630
|
|
|
587
|
-
this.signaling!.once(
|
|
631
|
+
this.signaling!.once("answer_sdp", async ({ result, answer_sdp }) => {
|
|
588
632
|
clearTimeout(timeout);
|
|
589
633
|
if (!result) {
|
|
590
|
-
reject(new Error(
|
|
634
|
+
reject(new Error("Failed to get SDP answer"));
|
|
591
635
|
return;
|
|
592
636
|
}
|
|
593
637
|
|
|
594
638
|
try {
|
|
595
|
-
await pc.setRemoteDescription({ type:
|
|
639
|
+
await pc.setRemoteDescription({ type: "answer", sdp: answer_sdp });
|
|
596
640
|
resolve();
|
|
597
641
|
} catch (err) {
|
|
598
642
|
reject(err);
|
|
@@ -605,49 +649,49 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
605
649
|
if (!this.signaling) return;
|
|
606
650
|
|
|
607
651
|
// Dispatch webrtc_connected event (P2)
|
|
608
|
-
this.signaling.on(
|
|
652
|
+
this.signaling.on("connected", () => {
|
|
609
653
|
if (this.destroyed) return;
|
|
610
|
-
video.dispatchEvent(new Event(
|
|
654
|
+
video.dispatchEvent(new Event("webrtc_connected"));
|
|
611
655
|
});
|
|
612
656
|
|
|
613
|
-
this.signaling.on(
|
|
657
|
+
this.signaling.on("time_update", (update: MistTimeUpdate) => {
|
|
614
658
|
if (this.destroyed) return;
|
|
615
659
|
this.handleTimeUpdate(update, video);
|
|
616
660
|
});
|
|
617
661
|
|
|
618
|
-
this.signaling.on(
|
|
662
|
+
this.signaling.on("seeked", ({ live_point }) => {
|
|
619
663
|
if (this.destroyed) return;
|
|
620
664
|
// Dispatch seeked event
|
|
621
|
-
video.dispatchEvent(new CustomEvent(
|
|
665
|
+
video.dispatchEvent(new CustomEvent("seeked", { detail: { seekOffset: this.seekOffset } }));
|
|
622
666
|
// Set playback rate to auto if seeked to live point
|
|
623
667
|
if (live_point && this.signaling) {
|
|
624
|
-
this.signaling.setSpeed(
|
|
668
|
+
this.signaling.setSpeed("auto");
|
|
625
669
|
}
|
|
626
670
|
video.play().catch(() => {});
|
|
627
671
|
});
|
|
628
672
|
|
|
629
|
-
this.signaling.on(
|
|
673
|
+
this.signaling.on("speed_changed", ({ play_rate_curr }) => {
|
|
630
674
|
if (this.destroyed) return;
|
|
631
675
|
this.playRate = play_rate_curr;
|
|
632
|
-
video.dispatchEvent(new CustomEvent(
|
|
676
|
+
video.dispatchEvent(new CustomEvent("ratechange", { detail: { play_rate_curr } }));
|
|
633
677
|
});
|
|
634
678
|
|
|
635
|
-
this.signaling.on(
|
|
679
|
+
this.signaling.on("stopped", () => {
|
|
636
680
|
if (this.destroyed) return;
|
|
637
681
|
this.isLiveStream = false;
|
|
638
682
|
video.pause();
|
|
639
|
-
this.emit(
|
|
683
|
+
this.emit("ended", undefined);
|
|
640
684
|
});
|
|
641
685
|
|
|
642
|
-
this.signaling.on(
|
|
686
|
+
this.signaling.on("error", ({ message }) => {
|
|
643
687
|
if (this.destroyed) return;
|
|
644
|
-
this.emit(
|
|
688
|
+
this.emit("error", message);
|
|
645
689
|
});
|
|
646
690
|
|
|
647
691
|
// Dispatch webrtc_disconnected event (P2)
|
|
648
|
-
this.signaling.on(
|
|
692
|
+
this.signaling.on("disconnected", () => {
|
|
649
693
|
if (this.destroyed) return;
|
|
650
|
-
video.dispatchEvent(new Event(
|
|
694
|
+
video.dispatchEvent(new Event("webrtc_disconnected"));
|
|
651
695
|
video.pause();
|
|
652
696
|
});
|
|
653
697
|
}
|
|
@@ -668,9 +712,11 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
668
712
|
if (update.tracks && !this.arraysEqual(update.tracks, this.currentTracks)) {
|
|
669
713
|
for (const trackId of update.tracks) {
|
|
670
714
|
if (!this.currentTracks.includes(trackId)) {
|
|
671
|
-
video.dispatchEvent(
|
|
672
|
-
|
|
673
|
-
|
|
715
|
+
video.dispatchEvent(
|
|
716
|
+
new CustomEvent("playerUpdate_trackChanged", {
|
|
717
|
+
detail: { trackId },
|
|
718
|
+
})
|
|
719
|
+
);
|
|
674
720
|
}
|
|
675
721
|
}
|
|
676
722
|
this.currentTracks = [...update.tracks];
|
|
@@ -686,8 +732,8 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
686
732
|
if (!this.signaling) return;
|
|
687
733
|
|
|
688
734
|
// Add transceivers for receiving
|
|
689
|
-
pc.addTransceiver(
|
|
690
|
-
pc.addTransceiver(
|
|
735
|
+
pc.addTransceiver("video", { direction: "recvonly" });
|
|
736
|
+
pc.addTransceiver("audio", { direction: "recvonly" });
|
|
691
737
|
|
|
692
738
|
const offer = await pc.createOffer({
|
|
693
739
|
offerToReceiveAudio: true,
|