@livepeer-frameworks/player-core 0.0.4 → 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 +21 -6
- package/dist/cjs/index.js +792 -146
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +792 -146
- package/dist/esm/index.js.map +1 -1
- package/dist/player.css +185 -373
- package/dist/types/core/GatewayClient.d.ts +3 -4
- package/dist/types/core/InteractionController.d.ts +12 -0
- package/dist/types/core/MetaTrackManager.d.ts +1 -1
- package/dist/types/core/PlayerController.d.ts +18 -2
- package/dist/types/core/PlayerInterface.d.ts +10 -0
- package/dist/types/core/SeekingUtils.d.ts +3 -1
- package/dist/types/core/StreamStateClient.d.ts +1 -1
- package/dist/types/players/HlsJsPlayer.d.ts +8 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +1 -1
- package/dist/types/players/VideoJsPlayer.d.ts +12 -4
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/index.d.ts +11 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +25 -3
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +20 -2
- package/dist/types/types.d.ts +32 -1
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +5 -5
- package/dist/types/vanilla/index.d.ts +3 -3
- package/dist/workers/decoder.worker.js +183 -6
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ABRController.ts +38 -36
- package/src/core/CodecUtils.ts +50 -47
- package/src/core/Disposable.ts +4 -4
- package/src/core/EventEmitter.ts +1 -1
- package/src/core/GatewayClient.ts +48 -48
- package/src/core/InteractionController.ts +89 -82
- package/src/core/LiveDurationProxy.ts +14 -16
- package/src/core/MetaTrackManager.ts +74 -66
- package/src/core/MistReporter.ts +72 -45
- package/src/core/MistSignaling.ts +59 -56
- package/src/core/PlayerController.ts +724 -375
- package/src/core/PlayerInterface.ts +89 -59
- package/src/core/PlayerManager.ts +118 -123
- 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 +75 -69
- package/src/core/SubtitleManager.ts +25 -23
- package/src/core/TelemetryReporter.ts +34 -31
- package/src/core/TimeFormat.ts +13 -17
- package/src/core/TimerManager.ts +25 -9
- 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 +137 -138
- 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 +175 -114
- package/src/players/HlsJsPlayer.ts +154 -76
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +44 -39
- package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -10
- package/src/players/MewsWsPlayer/index.ts +196 -154
- package/src/players/MewsWsPlayer/types.ts +21 -21
- package/src/players/MistPlayer.ts +46 -27
- package/src/players/MistWebRTCPlayer/index.ts +175 -129
- package/src/players/NativePlayer.ts +203 -143
- package/src/players/VideoJsPlayer.ts +200 -146
- 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 +46 -55
- package/src/players/WebCodecsPlayer/WebSocketController.ts +67 -69
- package/src/players/WebCodecsPlayer/index.ts +280 -220
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
- package/src/players/WebCodecsPlayer/types.ts +81 -53
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +255 -192
- package/src/players/WebCodecsPlayer/worker/types.ts +33 -29
- package/src/players/index.ts +8 -8
- package/src/styles/animations.css +2 -1
- package/src/styles/player.css +182 -356
- package/src/styles/tailwind.css +473 -159
- package/src/types.ts +75 -33
- package/src/vanilla/FrameWorksPlayer.ts +34 -19
- package/src/vanilla/index.ts +7 -7
|
@@ -14,36 +14,38 @@
|
|
|
14
14
|
* Protocol: MistServer raw WebSocket frames (12-byte header + data)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { BasePlayer } from
|
|
17
|
+
import { BasePlayer } from "../../core/PlayerInterface";
|
|
18
18
|
import type {
|
|
19
19
|
StreamSource,
|
|
20
20
|
StreamInfo,
|
|
21
21
|
PlayerOptions,
|
|
22
22
|
PlayerCapability,
|
|
23
|
-
} from
|
|
23
|
+
} from "../../core/PlayerInterface";
|
|
24
24
|
import type {
|
|
25
25
|
TrackInfo,
|
|
26
26
|
CodecDataMessage,
|
|
27
27
|
InfoMessage,
|
|
28
28
|
OnTimeMessage,
|
|
29
29
|
RawChunk,
|
|
30
|
-
LatencyProfileName,
|
|
31
30
|
WebCodecsPlayerOptions,
|
|
32
31
|
WebCodecsStats,
|
|
33
32
|
MainToWorkerMessage,
|
|
34
33
|
WorkerToMainMessage,
|
|
35
|
-
} from
|
|
36
|
-
import { WebSocketController } from
|
|
37
|
-
import { SyncController } from
|
|
38
|
-
import { getPresentationTimestamp, isInitData } from
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
34
|
+
} from "./types";
|
|
35
|
+
import { WebSocketController } from "./WebSocketController";
|
|
36
|
+
import { SyncController } from "./SyncController";
|
|
37
|
+
import { getPresentationTimestamp, isInitData } from "./RawChunkParser";
|
|
38
|
+
import { mergeLatencyProfile, selectDefaultProfile } from "./LatencyProfiles";
|
|
39
|
+
import {
|
|
40
|
+
createTrackGenerator,
|
|
41
|
+
hasNativeMediaStreamTrackGenerator,
|
|
42
|
+
} from "./polyfills/MediaStreamTrackGenerator";
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Detect if running on Safari (which has VideoTrackGenerator in worker but not MediaStreamTrackGenerator on main thread)
|
|
44
46
|
*/
|
|
45
47
|
function isSafari(): boolean {
|
|
46
|
-
if (typeof navigator ===
|
|
48
|
+
if (typeof navigator === "undefined") return false;
|
|
47
49
|
const ua = navigator.userAgent;
|
|
48
50
|
return /^((?!chrome|android).)*safari/i.test(ua);
|
|
49
51
|
}
|
|
@@ -69,11 +71,11 @@ function createTimeRanges(ranges: [number, number][]): TimeRanges {
|
|
|
69
71
|
return {
|
|
70
72
|
length: ranges.length,
|
|
71
73
|
start(index: number): number {
|
|
72
|
-
if (index < 0 || index >= ranges.length) throw new DOMException(
|
|
74
|
+
if (index < 0 || index >= ranges.length) throw new DOMException("Index out of bounds");
|
|
73
75
|
return ranges[index][0];
|
|
74
76
|
},
|
|
75
77
|
end(index: number): number {
|
|
76
|
-
if (index < 0 || index >= ranges.length) throw new DOMException(
|
|
78
|
+
if (index < 0 || index >= ranges.length) throw new DOMException("Index out of bounds");
|
|
77
79
|
return ranges[index][1];
|
|
78
80
|
},
|
|
79
81
|
};
|
|
@@ -111,14 +113,18 @@ interface PipelineInfo {
|
|
|
111
113
|
*/
|
|
112
114
|
export class WebCodecsPlayerImpl extends BasePlayer {
|
|
113
115
|
readonly capability: PlayerCapability = {
|
|
114
|
-
name:
|
|
115
|
-
shortname:
|
|
116
|
+
name: "WebCodecs Player",
|
|
117
|
+
shortname: "webcodecs",
|
|
116
118
|
priority: 0, // Highest priority - lowest latency option
|
|
117
|
-
// Raw WebSocket (12-byte header +
|
|
119
|
+
// Raw WebSocket (12-byte header + codec frames) - NOT MP4-muxed
|
|
118
120
|
// MistServer's output_wsraw.cpp provides full codec negotiation (audio + video)
|
|
121
|
+
// MistServer's output_h264.cpp uses same 12-byte header but Annex B payload (video-only)
|
|
119
122
|
// NOTE: ws/video/mp4 is MP4-fragmented which needs MEWS player (uses MSE)
|
|
120
123
|
mimes: [
|
|
121
|
-
|
|
124
|
+
"ws/video/raw",
|
|
125
|
+
"wss/video/raw", // Raw codec frames - AVCC format (audio + video)
|
|
126
|
+
"ws/video/h264",
|
|
127
|
+
"wss/video/h264", // Annex B H264/HEVC (video-only, same 12-byte header)
|
|
122
128
|
],
|
|
123
129
|
};
|
|
124
130
|
|
|
@@ -135,7 +141,9 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
135
141
|
private isDestroyed = false;
|
|
136
142
|
private debugging = false;
|
|
137
143
|
private verboseDebugging = false;
|
|
138
|
-
private streamType:
|
|
144
|
+
private streamType: "live" | "vod" = "live";
|
|
145
|
+
/** Payload format: 'avcc' for ws/video/raw, 'annexb' for ws/video/h264 */
|
|
146
|
+
private payloadFormat: "avcc" | "annexb" = "avcc";
|
|
139
147
|
private workerUidCounter = 0;
|
|
140
148
|
private workerListeners = new Map<number, (msg: WorkerToMainMessage) => void>();
|
|
141
149
|
|
|
@@ -163,11 +171,18 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
163
171
|
/**
|
|
164
172
|
* Get cache key for a track's codec configuration
|
|
165
173
|
*/
|
|
166
|
-
private static getCodecCacheKey(track: {
|
|
167
|
-
|
|
174
|
+
private static getCodecCacheKey(track: {
|
|
175
|
+
codec: string;
|
|
176
|
+
codecstring?: string;
|
|
177
|
+
init?: string;
|
|
178
|
+
}): string {
|
|
179
|
+
const codecStr = track.codecstring ?? track.codec?.toLowerCase() ?? "";
|
|
168
180
|
// Simple hash of init data for cache key (just first/last bytes + length)
|
|
169
|
-
const init = track.init ??
|
|
170
|
-
const initHash =
|
|
181
|
+
const init = track.init ?? "";
|
|
182
|
+
const initHash =
|
|
183
|
+
init.length > 0
|
|
184
|
+
? `${init.length}_${init.charCodeAt(0)}_${init.charCodeAt(init.length - 1)}`
|
|
185
|
+
: "";
|
|
171
186
|
return `${codecStr}|${initHash}`;
|
|
172
187
|
}
|
|
173
188
|
|
|
@@ -185,11 +200,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
// Build codec config
|
|
188
|
-
const codecStr = track.codecstring ?? (track.codec ??
|
|
203
|
+
const codecStr = track.codecstring ?? (track.codec ?? "").toLowerCase();
|
|
189
204
|
const config: any = { codec: codecStr };
|
|
190
205
|
|
|
191
206
|
// Add description (init data) if present
|
|
192
|
-
if (track.init && track.init !==
|
|
207
|
+
if (track.init && track.init !== "") {
|
|
193
208
|
config.description = str2bin(track.init);
|
|
194
209
|
}
|
|
195
210
|
|
|
@@ -197,27 +212,29 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
197
212
|
|
|
198
213
|
try {
|
|
199
214
|
switch (track.type) {
|
|
200
|
-
case
|
|
215
|
+
case "video": {
|
|
201
216
|
// Special handling for JPEG - uses ImageDecoder
|
|
202
|
-
if (track.codec ===
|
|
203
|
-
if (!(
|
|
204
|
-
result = { supported: false, config: { codec:
|
|
217
|
+
if (track.codec === "JPEG") {
|
|
218
|
+
if (!("ImageDecoder" in window)) {
|
|
219
|
+
result = { supported: false, config: { codec: "image/jpeg" } };
|
|
205
220
|
} else {
|
|
206
221
|
// @ts-ignore - ImageDecoder may not have types
|
|
207
|
-
const isSupported = await (window as any).ImageDecoder.isTypeSupported(
|
|
208
|
-
result = { supported: isSupported, config: { codec:
|
|
222
|
+
const isSupported = await (window as any).ImageDecoder.isTypeSupported("image/jpeg");
|
|
223
|
+
result = { supported: isSupported, config: { codec: "image/jpeg" } };
|
|
209
224
|
}
|
|
210
225
|
} else {
|
|
211
226
|
// Use VideoDecoder.isConfigSupported()
|
|
212
|
-
|
|
227
|
+
const videoResult = await VideoDecoder.isConfigSupported(config as VideoDecoderConfig);
|
|
228
|
+
result = { supported: videoResult.supported === true, config: videoResult.config };
|
|
213
229
|
}
|
|
214
230
|
break;
|
|
215
231
|
}
|
|
216
|
-
case
|
|
232
|
+
case "audio": {
|
|
217
233
|
// Audio requires numberOfChannels and sampleRate
|
|
218
234
|
config.numberOfChannels = track.channels ?? 2;
|
|
219
235
|
config.sampleRate = track.rate ?? 48000;
|
|
220
|
-
|
|
236
|
+
const audioResult = await AudioDecoder.isConfigSupported(config as AudioDecoderConfig);
|
|
237
|
+
result = { supported: audioResult.supported === true, config: audioResult.config };
|
|
221
238
|
break;
|
|
222
239
|
}
|
|
223
240
|
default:
|
|
@@ -241,7 +258,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
241
258
|
const supportedTypes: Set<string> = new Set();
|
|
242
259
|
|
|
243
260
|
const validationPromises = tracks
|
|
244
|
-
.filter(t => t.type ===
|
|
261
|
+
.filter((t) => t.type === "video" || t.type === "audio")
|
|
245
262
|
.map(async (track) => {
|
|
246
263
|
const result = await WebCodecsPlayerImpl.isTrackSupported(track);
|
|
247
264
|
if (result.supported) {
|
|
@@ -254,7 +271,9 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
254
271
|
|
|
255
272
|
// Log validation results for debugging
|
|
256
273
|
for (const { track, supported } of results) {
|
|
257
|
-
console.debug(
|
|
274
|
+
console.debug(
|
|
275
|
+
`[WebCodecs] Track ${track.idx} (${track.type} ${track.codec}): ${supported ? "supported" : "UNSUPPORTED"}`
|
|
276
|
+
);
|
|
258
277
|
}
|
|
259
278
|
|
|
260
279
|
return Array.from(supportedTypes);
|
|
@@ -270,20 +289,20 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
270
289
|
streamInfo: StreamInfo
|
|
271
290
|
): boolean | string[] {
|
|
272
291
|
// Basic requirements
|
|
273
|
-
if (!(
|
|
292
|
+
if (!("WebSocket" in window)) {
|
|
274
293
|
return false;
|
|
275
294
|
}
|
|
276
|
-
if (!(
|
|
295
|
+
if (!("Worker" in window)) {
|
|
277
296
|
return false;
|
|
278
297
|
}
|
|
279
|
-
if (!(
|
|
298
|
+
if (!("VideoDecoder" in window) || !("AudioDecoder" in window)) {
|
|
280
299
|
// WebCodecs not available (requires HTTPS)
|
|
281
300
|
return false;
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
// Check for HTTP/HTTPS mismatch
|
|
285
|
-
const sourceUrl = new URL(source.url.replace(/^ws/,
|
|
286
|
-
if (location.protocol ===
|
|
304
|
+
const sourceUrl = new URL(source.url.replace(/^ws/, "http"), location.href);
|
|
305
|
+
if (location.protocol === "https:" && sourceUrl.protocol === "http:") {
|
|
287
306
|
return false;
|
|
288
307
|
}
|
|
289
308
|
|
|
@@ -292,7 +311,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
292
311
|
const playableTracks: Record<string, boolean> = {};
|
|
293
312
|
|
|
294
313
|
for (const track of streamInfo.meta.tracks) {
|
|
295
|
-
if (track.type ===
|
|
314
|
+
if (track.type === "video" || track.type === "audio") {
|
|
296
315
|
// Check cache for this track's codec
|
|
297
316
|
const cacheKey = WebCodecsPlayerImpl.getCodecCacheKey(track as any);
|
|
298
317
|
if (WebCodecsPlayerImpl.codecCache.has(cacheKey)) {
|
|
@@ -305,12 +324,17 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
305
324
|
// This is necessary because isBrowserSupported is synchronous
|
|
306
325
|
playableTracks[track.type] = true;
|
|
307
326
|
}
|
|
308
|
-
} else if (track.type ===
|
|
327
|
+
} else if (track.type === "meta" && track.codec === "subtitle") {
|
|
309
328
|
// Subtitles supported via text track
|
|
310
|
-
playableTracks[
|
|
329
|
+
playableTracks["subtitle"] = true;
|
|
311
330
|
}
|
|
312
331
|
}
|
|
313
332
|
|
|
333
|
+
// Annex B H264 WebSocket is video-only (no audio payloads)
|
|
334
|
+
if (mimetype.includes("video/h264")) {
|
|
335
|
+
delete playableTracks.audio;
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
if (Object.keys(playableTracks).length === 0) {
|
|
315
339
|
return false;
|
|
316
340
|
}
|
|
@@ -341,8 +365,15 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
341
365
|
this._bytesReceived = 0;
|
|
342
366
|
this._messagesReceived = 0;
|
|
343
367
|
|
|
368
|
+
// Detect payload format from source MIME type
|
|
369
|
+
// ws/video/h264 uses Annex B (start code delimited NALs), ws/video/raw uses AVCC (length-prefixed)
|
|
370
|
+
this.payloadFormat = source.type?.includes("h264") ? "annexb" : "avcc";
|
|
371
|
+
if (this.payloadFormat === "annexb") {
|
|
372
|
+
this.log("Using Annex B payload format (ws/video/h264)");
|
|
373
|
+
}
|
|
374
|
+
|
|
344
375
|
this.container = container;
|
|
345
|
-
container.classList.add(
|
|
376
|
+
container.classList.add("fw-player-container");
|
|
346
377
|
|
|
347
378
|
// Pre-populate track metadata from streamInfo (fetched via HTTP before WebSocket)
|
|
348
379
|
// This is how the reference player (rawws.js) gets track info - from MistVideo.info.meta.tracks
|
|
@@ -353,7 +384,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
353
384
|
// Convert StreamTrack to TrackInfo (WebCodecs format)
|
|
354
385
|
const trackInfo: TrackInfo = {
|
|
355
386
|
idx: track.idx,
|
|
356
|
-
type: track.type as TrackInfo[
|
|
387
|
+
type: track.type as TrackInfo["type"],
|
|
357
388
|
codec: track.codec,
|
|
358
389
|
codecstring: track.codecstring,
|
|
359
390
|
init: track.init,
|
|
@@ -376,24 +407,25 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
376
407
|
this.verboseDebugging = wcOptions.verboseDebug ?? false;
|
|
377
408
|
|
|
378
409
|
// Determine stream type
|
|
379
|
-
this.streamType = (source as any).type ===
|
|
410
|
+
this.streamType = (source as any).type === "live" ? "live" : "vod";
|
|
380
411
|
|
|
381
412
|
// Select latency profile
|
|
382
|
-
const profileName =
|
|
413
|
+
const profileName =
|
|
414
|
+
wcOptions.latencyProfile ?? selectDefaultProfile(this.streamType === "live");
|
|
383
415
|
const profile = mergeLatencyProfile(profileName, wcOptions.customLatencyProfile);
|
|
384
416
|
|
|
385
417
|
this.log(`Initializing WebCodecs player with ${profile.name} profile`);
|
|
386
418
|
|
|
387
419
|
// Create video element
|
|
388
|
-
const video = document.createElement(
|
|
389
|
-
video.classList.add(
|
|
390
|
-
video.setAttribute(
|
|
391
|
-
video.setAttribute(
|
|
420
|
+
const video = document.createElement("video");
|
|
421
|
+
video.classList.add("fw-player-video");
|
|
422
|
+
video.setAttribute("playsinline", "");
|
|
423
|
+
video.setAttribute("crossorigin", "anonymous");
|
|
392
424
|
|
|
393
425
|
if (options.autoplay) video.autoplay = true;
|
|
394
426
|
if (options.muted) video.muted = true;
|
|
395
427
|
video.controls = options.controls === true;
|
|
396
|
-
if (options.loop && this.streamType !==
|
|
428
|
+
if (options.loop && this.streamType !== "live") video.loop = true;
|
|
397
429
|
if (options.poster) video.poster = options.poster;
|
|
398
430
|
|
|
399
431
|
this.videoElement = video;
|
|
@@ -404,8 +436,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
404
436
|
if (this._suppressPlayPauseSync) return;
|
|
405
437
|
this._isPaused = false;
|
|
406
438
|
this.sendToWorker({
|
|
407
|
-
type:
|
|
408
|
-
action:
|
|
439
|
+
type: "frametiming",
|
|
440
|
+
action: "setPaused",
|
|
409
441
|
paused: false,
|
|
410
442
|
uid: this.workerUidCounter++,
|
|
411
443
|
}).catch(() => {});
|
|
@@ -414,14 +446,14 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
414
446
|
if (this._suppressPlayPauseSync) return;
|
|
415
447
|
this._isPaused = true;
|
|
416
448
|
this.sendToWorker({
|
|
417
|
-
type:
|
|
418
|
-
action:
|
|
449
|
+
type: "frametiming",
|
|
450
|
+
action: "setPaused",
|
|
419
451
|
paused: true,
|
|
420
452
|
uid: this.workerUidCounter++,
|
|
421
453
|
}).catch(() => {});
|
|
422
454
|
};
|
|
423
|
-
video.addEventListener(
|
|
424
|
-
video.addEventListener(
|
|
455
|
+
video.addEventListener("play", this._onVideoPlay);
|
|
456
|
+
video.addEventListener("pause", this._onVideoPause);
|
|
425
457
|
|
|
426
458
|
// Create MediaStream for output
|
|
427
459
|
this.mediaStream = new MediaStream();
|
|
@@ -433,11 +465,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
433
465
|
// Initialize sync controller
|
|
434
466
|
this.syncController = new SyncController({
|
|
435
467
|
profile,
|
|
436
|
-
isLive: this.streamType ===
|
|
468
|
+
isLive: this.streamType === "live",
|
|
437
469
|
onSpeedChange: (main, tweak) => {
|
|
438
470
|
this.sendToWorker({
|
|
439
|
-
type:
|
|
440
|
-
action:
|
|
471
|
+
type: "frametiming",
|
|
472
|
+
action: "setSpeed",
|
|
441
473
|
speed: main,
|
|
442
474
|
tweak,
|
|
443
475
|
uid: this.workerUidCounter++,
|
|
@@ -465,13 +497,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
465
497
|
const supportedVideoCodecs: Set<string> = new Set();
|
|
466
498
|
|
|
467
499
|
if (streamInfo?.meta?.tracks) {
|
|
468
|
-
this.log(
|
|
500
|
+
this.log("Validating track codecs with isConfigSupported()...");
|
|
469
501
|
|
|
470
502
|
for (const track of streamInfo.meta.tracks) {
|
|
471
|
-
if (track.type ===
|
|
503
|
+
if (track.type === "video" || track.type === "audio") {
|
|
472
504
|
const trackInfo: TrackInfo = {
|
|
473
505
|
idx: track.idx ?? 0,
|
|
474
|
-
type: track.type as
|
|
506
|
+
type: track.type as "video" | "audio",
|
|
475
507
|
codec: track.codec,
|
|
476
508
|
codecstring: track.codecstring,
|
|
477
509
|
init: track.init,
|
|
@@ -483,14 +515,14 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
483
515
|
|
|
484
516
|
const result = await WebCodecsPlayerImpl.isTrackSupported(trackInfo);
|
|
485
517
|
if (result.supported) {
|
|
486
|
-
if (track.type ===
|
|
518
|
+
if (track.type === "audio") {
|
|
487
519
|
supportedAudioCodecs.add(track.codec);
|
|
488
520
|
} else {
|
|
489
521
|
supportedVideoCodecs.add(track.codec);
|
|
490
522
|
}
|
|
491
523
|
this.log(`Track ${track.idx} (${track.type} ${track.codec}): SUPPORTED`);
|
|
492
524
|
} else {
|
|
493
|
-
this.log(`Track ${track.idx} (${track.type} ${track.codec}): NOT SUPPORTED`,
|
|
525
|
+
this.log(`Track ${track.idx} (${track.type} ${track.codec}): NOT SUPPORTED`, "warn");
|
|
494
526
|
}
|
|
495
527
|
}
|
|
496
528
|
}
|
|
@@ -500,34 +532,38 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
500
532
|
if (supportedAudioCodecs.size === 0 && supportedVideoCodecs.size === 0) {
|
|
501
533
|
// Fallback: Use default codec list if no tracks provided or all failed
|
|
502
534
|
// This handles streams where track info isn't available until WebSocket connects
|
|
503
|
-
this.log(
|
|
504
|
-
[
|
|
505
|
-
[
|
|
535
|
+
this.log("No validated codecs, using default codec list");
|
|
536
|
+
["AAC", "MP3", "opus", "FLAC", "AC3"].forEach((c) => supportedAudioCodecs.add(c));
|
|
537
|
+
["H264", "HEVC", "VP8", "VP9", "AV1", "JPEG"].forEach((c) => supportedVideoCodecs.add(c));
|
|
506
538
|
}
|
|
507
539
|
|
|
508
540
|
// Connect and request codec data
|
|
509
541
|
// Per MistServer rawws.js line 1544, we need to tell the server what codecs we support
|
|
510
542
|
// Format: [[ [audio codecs], [video codecs] ]] - audio FIRST per Object.values({audio:[], video:[]}) order
|
|
511
|
-
const supportedCombinations: string[][][] = [
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
543
|
+
const supportedCombinations: string[][][] = [
|
|
544
|
+
[
|
|
545
|
+
Array.from(supportedAudioCodecs), // Audio codecs (position 0)
|
|
546
|
+
Array.from(supportedVideoCodecs), // Video codecs (position 1)
|
|
547
|
+
],
|
|
548
|
+
];
|
|
515
549
|
|
|
516
|
-
this.log(
|
|
550
|
+
this.log(
|
|
551
|
+
`Requesting codecs: audio=[${supportedCombinations[0][0].join(", ")}], video=[${supportedCombinations[0][1].join(", ")}]`
|
|
552
|
+
);
|
|
517
553
|
|
|
518
554
|
try {
|
|
519
555
|
await this.wsController.connect();
|
|
520
556
|
this.wsController.requestCodecData(supportedCombinations);
|
|
521
557
|
} catch (err) {
|
|
522
|
-
this.log(`Failed to connect: ${err}`,
|
|
523
|
-
this.emit(
|
|
558
|
+
this.log(`Failed to connect: ${err}`, "error");
|
|
559
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
524
560
|
throw err;
|
|
525
561
|
}
|
|
526
562
|
|
|
527
563
|
// Proactively create pipelines for pre-populated tracks
|
|
528
564
|
// This ensures pipelines exist when first chunks arrive, they just need init data
|
|
529
565
|
for (const [idx, track] of this.tracksByIndex) {
|
|
530
|
-
if (track.type ===
|
|
566
|
+
if (track.type === "video" || track.type === "audio") {
|
|
531
567
|
this.log(`Creating pipeline proactively for track ${idx} (${track.type} ${track.codec})`);
|
|
532
568
|
await this.createPipeline(track);
|
|
533
569
|
}
|
|
@@ -547,7 +583,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
547
583
|
if (this.isDestroyed) return;
|
|
548
584
|
this.isDestroyed = true;
|
|
549
585
|
|
|
550
|
-
this.log(
|
|
586
|
+
this.log("Destroying WebCodecs player");
|
|
551
587
|
|
|
552
588
|
// Cancel frame callback
|
|
553
589
|
this.cancelFrameCallback();
|
|
@@ -585,11 +621,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
585
621
|
// Clean up video element
|
|
586
622
|
if (this.videoElement) {
|
|
587
623
|
if (this._onVideoPlay) {
|
|
588
|
-
this.videoElement.removeEventListener(
|
|
624
|
+
this.videoElement.removeEventListener("play", this._onVideoPlay);
|
|
589
625
|
this._onVideoPlay = undefined;
|
|
590
626
|
}
|
|
591
627
|
if (this._onVideoPause) {
|
|
592
|
-
this.videoElement.removeEventListener(
|
|
628
|
+
this.videoElement.removeEventListener("pause", this._onVideoPause);
|
|
593
629
|
this._onVideoPause = undefined;
|
|
594
630
|
}
|
|
595
631
|
if (this._stepPauseTimeout) {
|
|
@@ -620,7 +656,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
620
656
|
return new Promise((resolve, reject) => {
|
|
621
657
|
let worker: Worker;
|
|
622
658
|
try {
|
|
623
|
-
worker = new Worker(url, { type:
|
|
659
|
+
worker = new Worker(url, { type: "module" });
|
|
624
660
|
} catch (e) {
|
|
625
661
|
reject(e);
|
|
626
662
|
return;
|
|
@@ -628,14 +664,14 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
628
664
|
|
|
629
665
|
const cleanup = () => {
|
|
630
666
|
clearTimeout(timeout);
|
|
631
|
-
worker.removeEventListener(
|
|
632
|
-
worker.removeEventListener(
|
|
667
|
+
worker.removeEventListener("error", onError);
|
|
668
|
+
worker.removeEventListener("message", onMessage);
|
|
633
669
|
};
|
|
634
670
|
|
|
635
671
|
const onError = (e: ErrorEvent) => {
|
|
636
672
|
cleanup();
|
|
637
673
|
worker.terminate();
|
|
638
|
-
reject(new Error(e.message ||
|
|
674
|
+
reject(new Error(e.message || "Worker failed to load"));
|
|
639
675
|
};
|
|
640
676
|
|
|
641
677
|
const onMessage = () => {
|
|
@@ -649,8 +685,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
649
685
|
resolve(worker);
|
|
650
686
|
}, 500);
|
|
651
687
|
|
|
652
|
-
worker.addEventListener(
|
|
653
|
-
worker.addEventListener(
|
|
688
|
+
worker.addEventListener("error", onError);
|
|
689
|
+
worker.addEventListener("message", onMessage);
|
|
654
690
|
});
|
|
655
691
|
}
|
|
656
692
|
|
|
@@ -658,13 +694,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
658
694
|
// Worker paths to try in order:
|
|
659
695
|
// 1. Dev server path (Vite plugin serves /workers/* from source)
|
|
660
696
|
// 2. Production npm package path (relative to built module)
|
|
661
|
-
const paths = [
|
|
662
|
-
'/workers/decoder.worker.js',
|
|
663
|
-
];
|
|
697
|
+
const paths = ["/workers/decoder.worker.js"];
|
|
664
698
|
|
|
665
699
|
// Add production path (may fail in dev but that's ok)
|
|
666
700
|
try {
|
|
667
|
-
paths.push(new URL(
|
|
701
|
+
paths.push(new URL("../workers/decoder.worker.js", import.meta.url).href);
|
|
668
702
|
} catch {
|
|
669
703
|
// import.meta.url may not work in all environments
|
|
670
704
|
}
|
|
@@ -678,14 +712,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
678
712
|
break;
|
|
679
713
|
} catch (e) {
|
|
680
714
|
lastError = e instanceof Error ? e : new Error(String(e));
|
|
681
|
-
this.log(`Worker path failed: ${path} - ${lastError.message}`,
|
|
715
|
+
this.log(`Worker path failed: ${path} - ${lastError.message}`, "warn");
|
|
682
716
|
}
|
|
683
717
|
}
|
|
684
718
|
|
|
685
719
|
if (!this.worker) {
|
|
686
720
|
throw new Error(
|
|
687
|
-
|
|
688
|
-
`Last error: ${lastError?.message ?? 'unknown'}`
|
|
721
|
+
"Failed to initialize WebCodecs worker. " + `Last error: ${lastError?.message ?? "unknown"}`
|
|
689
722
|
);
|
|
690
723
|
}
|
|
691
724
|
|
|
@@ -695,28 +728,31 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
695
728
|
};
|
|
696
729
|
|
|
697
730
|
this.worker.onerror = (err) => {
|
|
698
|
-
this.log(`Worker error: ${err?.message ??
|
|
699
|
-
this.emit(
|
|
731
|
+
this.log(`Worker error: ${err?.message ?? "unknown error"}`, "error");
|
|
732
|
+
this.emit("error", new Error(`Worker error: ${err?.message ?? "unknown"}`));
|
|
700
733
|
};
|
|
701
734
|
|
|
702
735
|
// Configure debugging mode in worker
|
|
703
736
|
this.sendToWorker({
|
|
704
|
-
type:
|
|
705
|
-
value: this.verboseDebugging ?
|
|
737
|
+
type: "debugging",
|
|
738
|
+
value: this.verboseDebugging ? "verbose" : this.debugging,
|
|
706
739
|
uid: this.workerUidCounter++,
|
|
707
740
|
});
|
|
708
741
|
}
|
|
709
742
|
|
|
710
|
-
private sendToWorker(
|
|
743
|
+
private sendToWorker(
|
|
744
|
+
msg: MainToWorkerMessage & { uid: number },
|
|
745
|
+
transfer?: Transferable[]
|
|
746
|
+
): Promise<WorkerToMainMessage> {
|
|
711
747
|
return new Promise((resolve, reject) => {
|
|
712
748
|
// Reject with proper error if destroyed or no worker
|
|
713
749
|
// This prevents silent failures and allows callers to handle errors appropriately
|
|
714
750
|
if (this.isDestroyed) {
|
|
715
|
-
reject(new Error(
|
|
751
|
+
reject(new Error("Player destroyed"));
|
|
716
752
|
return;
|
|
717
753
|
}
|
|
718
754
|
if (!this.worker) {
|
|
719
|
-
reject(new Error(
|
|
755
|
+
reject(new Error("Worker not initialized"));
|
|
720
756
|
return;
|
|
721
757
|
}
|
|
722
758
|
|
|
@@ -725,7 +761,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
725
761
|
// Register listener for response
|
|
726
762
|
this.workerListeners.set(uid, (response) => {
|
|
727
763
|
this.workerListeners.delete(uid);
|
|
728
|
-
if (response.type ===
|
|
764
|
+
if (response.type === "ack" && response.status === "error") {
|
|
729
765
|
reject(new Error(response.error));
|
|
730
766
|
} else {
|
|
731
767
|
resolve(response);
|
|
@@ -748,7 +784,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
748
784
|
|
|
749
785
|
// Handle message by type
|
|
750
786
|
switch (msg.type) {
|
|
751
|
-
case
|
|
787
|
+
case "addtrack": {
|
|
752
788
|
const pipeline = this.pipelines.get(msg.idx);
|
|
753
789
|
if (pipeline && this.mediaStream) {
|
|
754
790
|
// If track was created in worker (Safari), use it directly
|
|
@@ -762,7 +798,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
762
798
|
break;
|
|
763
799
|
}
|
|
764
800
|
|
|
765
|
-
case
|
|
801
|
+
case "removetrack": {
|
|
766
802
|
const pipeline = this.pipelines.get(msg.idx);
|
|
767
803
|
if (pipeline?.generator && this.mediaStream) {
|
|
768
804
|
const track = pipeline.generator.getTrack();
|
|
@@ -771,80 +807,84 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
771
807
|
break;
|
|
772
808
|
}
|
|
773
809
|
|
|
774
|
-
case
|
|
810
|
+
case "setplaybackrate": {
|
|
775
811
|
if (this.videoElement) {
|
|
776
812
|
this.videoElement.playbackRate = msg.speed;
|
|
777
813
|
}
|
|
778
814
|
break;
|
|
779
815
|
}
|
|
780
816
|
|
|
781
|
-
case
|
|
782
|
-
if (msg.kind ===
|
|
817
|
+
case "sendevent": {
|
|
818
|
+
if (msg.kind === "timeupdate") {
|
|
783
819
|
if (this._pendingStepPause) {
|
|
784
820
|
this.finishStepPause();
|
|
785
821
|
}
|
|
786
|
-
if (typeof msg.time ===
|
|
822
|
+
if (typeof msg.time === "number" && Number.isFinite(msg.time)) {
|
|
787
823
|
this._currentTime = msg.time;
|
|
788
|
-
this.emit(
|
|
824
|
+
this.emit("timeupdate", this._currentTime);
|
|
789
825
|
} else if (this.videoElement) {
|
|
790
|
-
this.emit(
|
|
826
|
+
this.emit("timeupdate", this.videoElement.currentTime);
|
|
791
827
|
}
|
|
792
|
-
} else if (msg.kind ===
|
|
793
|
-
this.emit(
|
|
828
|
+
} else if (msg.kind === "error") {
|
|
829
|
+
this.emit("error", new Error(msg.message ?? "Unknown error"));
|
|
794
830
|
}
|
|
795
831
|
break;
|
|
796
832
|
}
|
|
797
833
|
|
|
798
|
-
case
|
|
834
|
+
case "writeframe": {
|
|
799
835
|
// Safari audio: worker sends frames via postMessage, we write them here
|
|
800
836
|
// Reference: rawws.js line 897-918
|
|
801
837
|
const pipeline = this.pipelines.get(msg.idx);
|
|
802
838
|
if (pipeline?.safariAudioWriter) {
|
|
803
839
|
const frame = msg.frame;
|
|
804
840
|
const frameUid = msg.uid;
|
|
805
|
-
pipeline.safariAudioWriter
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
841
|
+
pipeline.safariAudioWriter
|
|
842
|
+
.write(frame)
|
|
843
|
+
.then(() => {
|
|
844
|
+
this.worker?.postMessage({
|
|
845
|
+
type: "writeframe",
|
|
846
|
+
idx: msg.idx,
|
|
847
|
+
uid: frameUid,
|
|
848
|
+
status: "ok",
|
|
849
|
+
});
|
|
850
|
+
})
|
|
851
|
+
.catch((err: Error) => {
|
|
852
|
+
this.worker?.postMessage({
|
|
853
|
+
type: "writeframe",
|
|
854
|
+
idx: msg.idx,
|
|
855
|
+
uid: frameUid,
|
|
856
|
+
status: "error",
|
|
857
|
+
error: err.message,
|
|
858
|
+
});
|
|
819
859
|
});
|
|
820
|
-
});
|
|
821
860
|
} else {
|
|
822
861
|
this.worker?.postMessage({
|
|
823
|
-
type:
|
|
862
|
+
type: "writeframe",
|
|
824
863
|
idx: msg.idx,
|
|
825
864
|
uid: msg.uid,
|
|
826
|
-
status:
|
|
827
|
-
error:
|
|
865
|
+
status: "error",
|
|
866
|
+
error: "Pipeline not active or no audio writer",
|
|
828
867
|
});
|
|
829
868
|
}
|
|
830
869
|
break;
|
|
831
870
|
}
|
|
832
871
|
|
|
833
|
-
case
|
|
872
|
+
case "log": {
|
|
834
873
|
if (this.debugging) {
|
|
835
|
-
const level = (msg as any).level ??
|
|
836
|
-
const logFn =
|
|
874
|
+
const level = (msg as any).level ?? "info";
|
|
875
|
+
const logFn =
|
|
876
|
+
level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
837
877
|
logFn(`[WebCodecs Worker] ${msg.msg}`);
|
|
838
878
|
}
|
|
839
879
|
break;
|
|
840
880
|
}
|
|
841
881
|
|
|
842
|
-
case
|
|
882
|
+
case "stats": {
|
|
843
883
|
// Could emit stats for monitoring
|
|
844
884
|
break;
|
|
845
885
|
}
|
|
846
886
|
|
|
847
|
-
case
|
|
887
|
+
case "closed": {
|
|
848
888
|
this.pipelines.delete(msg.idx);
|
|
849
889
|
break;
|
|
850
890
|
}
|
|
@@ -858,28 +898,30 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
858
898
|
private setupWebSocketHandlers(): void {
|
|
859
899
|
if (!this.wsController) return;
|
|
860
900
|
|
|
861
|
-
this.wsController.on(
|
|
862
|
-
this.wsController.on(
|
|
863
|
-
this.wsController.on(
|
|
864
|
-
this.wsController.on(
|
|
865
|
-
this.wsController.on(
|
|
866
|
-
this.wsController.on(
|
|
867
|
-
this.wsController.on(
|
|
868
|
-
this.wsController.on(
|
|
901
|
+
this.wsController.on("codecdata", (msg) => this.handleCodecData(msg));
|
|
902
|
+
this.wsController.on("info", (msg) => this.handleInfo(msg));
|
|
903
|
+
this.wsController.on("ontime", (msg) => this.handleOnTime(msg));
|
|
904
|
+
this.wsController.on("tracks", (tracks) => this.handleTracksChange(tracks));
|
|
905
|
+
this.wsController.on("chunk", (chunk) => this.handleChunk(chunk));
|
|
906
|
+
this.wsController.on("stop", () => this.handleStop());
|
|
907
|
+
this.wsController.on("error", (err) => this.handleError(err));
|
|
908
|
+
this.wsController.on("statechange", (state) => {
|
|
869
909
|
this.log(`Connection state: ${state}`);
|
|
870
|
-
if (state ===
|
|
871
|
-
this.emit(
|
|
910
|
+
if (state === "error") {
|
|
911
|
+
this.emit("error", new Error("WebSocket connection failed"));
|
|
872
912
|
}
|
|
873
913
|
});
|
|
874
914
|
}
|
|
875
915
|
|
|
876
916
|
private async handleCodecData(msg: CodecDataMessage): Promise<void> {
|
|
877
917
|
const codecs = msg.codecs ?? [];
|
|
878
|
-
const trackIndices = msg.tracks ?? [];
|
|
879
|
-
this.log(
|
|
918
|
+
const trackIndices = msg.tracks ?? []; // Array of track indices (numbers), NOT TrackInfo
|
|
919
|
+
this.log(
|
|
920
|
+
`Received codec data: codecs=[${codecs.join(", ") || "none"}], tracks=[${trackIndices.join(", ") || "none"}]`
|
|
921
|
+
);
|
|
880
922
|
|
|
881
923
|
if (codecs.length === 0 || trackIndices.length === 0) {
|
|
882
|
-
this.log(
|
|
924
|
+
this.log("No playable codecs/tracks selected by server", "warn");
|
|
883
925
|
// Still start playback - info message may populate tracks later
|
|
884
926
|
this.wsController?.play();
|
|
885
927
|
return;
|
|
@@ -899,8 +941,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
899
941
|
// Create minimal track info - will be filled in by info message
|
|
900
942
|
this.tracksByIndex.set(trackIdx, {
|
|
901
943
|
idx: trackIdx,
|
|
902
|
-
type: codec.match(/^(H264|HEVC|VP[89]|AV1|JPEG)/i)
|
|
903
|
-
|
|
944
|
+
type: codec.match(/^(H264|HEVC|VP[89]|AV1|JPEG)/i)
|
|
945
|
+
? "video"
|
|
946
|
+
: codec.match(/^(AAC|MP3|opus|FLAC|AC3|pcm)/i)
|
|
947
|
+
? "audio"
|
|
948
|
+
: "meta",
|
|
904
949
|
codec,
|
|
905
950
|
});
|
|
906
951
|
}
|
|
@@ -911,7 +956,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
911
956
|
// Create pipelines for selected tracks that have metadata
|
|
912
957
|
for (const trackIdx of trackIndices) {
|
|
913
958
|
const track = this.tracksByIndex.get(trackIdx);
|
|
914
|
-
if (track && (track.type ===
|
|
959
|
+
if (track && (track.type === "video" || track.type === "audio")) {
|
|
915
960
|
await this.createPipeline(track);
|
|
916
961
|
}
|
|
917
962
|
}
|
|
@@ -925,14 +970,14 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
925
970
|
* This is sent by MistServer with full track information
|
|
926
971
|
*/
|
|
927
972
|
private async handleInfo(msg: InfoMessage): Promise<void> {
|
|
928
|
-
this.log(
|
|
973
|
+
this.log("Received stream info");
|
|
929
974
|
|
|
930
975
|
// Extract tracks from meta.tracks object
|
|
931
976
|
if (msg.meta?.tracks) {
|
|
932
977
|
const tracksObj = msg.meta.tracks;
|
|
933
978
|
this.log(`Info contains ${Object.keys(tracksObj).length} tracks`);
|
|
934
979
|
|
|
935
|
-
for (const [
|
|
980
|
+
for (const [_name, track] of Object.entries(tracksObj)) {
|
|
936
981
|
// Store track by its index for lookup when chunks arrive
|
|
937
982
|
if (track.idx !== undefined) {
|
|
938
983
|
this.tracksByIndex.set(track.idx, track);
|
|
@@ -940,7 +985,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
940
985
|
|
|
941
986
|
// Process any queued init data for this track
|
|
942
987
|
if (this.queuedInitData.has(track.idx)) {
|
|
943
|
-
if (track.type ===
|
|
988
|
+
if (track.type === "video" || track.type === "audio") {
|
|
944
989
|
this.log(`Processing queued INIT data for track ${track.idx}`);
|
|
945
990
|
await this.createPipeline(track);
|
|
946
991
|
const initData = this.queuedInitData.get(track.idx)!;
|
|
@@ -987,8 +1032,10 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
987
1032
|
for (const trackIdx of msg.tracks) {
|
|
988
1033
|
if (!this.pipelines.has(trackIdx)) {
|
|
989
1034
|
const track = this.tracksByIndex.get(trackIdx);
|
|
990
|
-
if (track && (track.type ===
|
|
991
|
-
this.log(
|
|
1035
|
+
if (track && (track.type === "video" || track.type === "audio")) {
|
|
1036
|
+
this.log(
|
|
1037
|
+
`Creating pipeline from on_time for track ${track.idx} (${track.type} ${track.codec})`
|
|
1038
|
+
);
|
|
992
1039
|
this.createPipeline(track).then(() => {
|
|
993
1040
|
// Process any queued init data
|
|
994
1041
|
const queuedInit = this.queuedInitData.get(track.idx);
|
|
@@ -1004,10 +1051,10 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1004
1051
|
}
|
|
1005
1052
|
|
|
1006
1053
|
private async handleTracksChange(tracks: TrackInfo[]): Promise<void> {
|
|
1007
|
-
this.log(`Tracks changed: ${tracks.map(t => `${t.idx}:${t.type}`).join(
|
|
1054
|
+
this.log(`Tracks changed: ${tracks.map((t) => `${t.idx}:${t.type}`).join(", ")}`);
|
|
1008
1055
|
|
|
1009
1056
|
// Check if codecs changed
|
|
1010
|
-
const newTrackIds = new Set(tracks.map(t => t.idx));
|
|
1057
|
+
const newTrackIds = new Set(tracks.map((t) => t.idx));
|
|
1011
1058
|
const oldTrackIds = new Set(this.pipelines.keys());
|
|
1012
1059
|
|
|
1013
1060
|
// Remove old pipelines
|
|
@@ -1021,7 +1068,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1021
1068
|
for (const track of tracks) {
|
|
1022
1069
|
this.tracksByIndex.set(track.idx, track);
|
|
1023
1070
|
|
|
1024
|
-
if (track.type ===
|
|
1071
|
+
if (track.type === "video" || track.type === "audio") {
|
|
1025
1072
|
if (!this.pipelines.has(track.idx)) {
|
|
1026
1073
|
await this.createPipeline(track);
|
|
1027
1074
|
}
|
|
@@ -1053,12 +1100,14 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1053
1100
|
}
|
|
1054
1101
|
|
|
1055
1102
|
// For regular chunks without track info, we can't decode without codec config
|
|
1056
|
-
this.log(`Received chunk for unknown track ${chunk.trackIndex} without track info`,
|
|
1103
|
+
this.log(`Received chunk for unknown track ${chunk.trackIndex} without track info`, "warn");
|
|
1057
1104
|
return;
|
|
1058
1105
|
}
|
|
1059
1106
|
|
|
1060
|
-
if (track.type ===
|
|
1061
|
-
this.log(
|
|
1107
|
+
if (track.type === "video" || track.type === "audio") {
|
|
1108
|
+
this.log(
|
|
1109
|
+
`Creating pipeline for discovered track ${track.idx} (${track.type} ${track.codec})`
|
|
1110
|
+
);
|
|
1062
1111
|
this.createPipeline(track).then(() => {
|
|
1063
1112
|
if (this.isDestroyed) return; // Guard against async completion after destroy
|
|
1064
1113
|
// Process any queued init data for this track
|
|
@@ -1086,14 +1135,16 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1086
1135
|
// For AUDIO tracks: configure on FIRST frame (audio doesn't have key/delta distinction)
|
|
1087
1136
|
// Audio chunks are sent as type 0 (delta) by the server even though they're independent
|
|
1088
1137
|
// Reference: rawws.js line 768-769 forces audio type to 'key'
|
|
1089
|
-
const isAudioTrack = pipeline.track.type ===
|
|
1138
|
+
const isAudioTrack = pipeline.track.type === "audio";
|
|
1090
1139
|
|
|
1091
1140
|
// For VIDEO tracks: wait for KEY frame before configuring
|
|
1092
1141
|
// This handles Annex B streams where SPS/PPS is inline with keyframes
|
|
1093
|
-
const shouldConfigure = isAudioTrack || chunk.type ===
|
|
1142
|
+
const shouldConfigure = isAudioTrack || chunk.type === "key";
|
|
1094
1143
|
|
|
1095
1144
|
if (shouldConfigure) {
|
|
1096
|
-
this.log(
|
|
1145
|
+
this.log(
|
|
1146
|
+
`Received ${chunk.type.toUpperCase()} frame for unconfigured ${pipeline.track.type} track ${chunk.trackIndex}, configuring`
|
|
1147
|
+
);
|
|
1097
1148
|
|
|
1098
1149
|
// Queue this frame at the FRONT so it's sent before any DELTAs
|
|
1099
1150
|
if (!this.queuedChunks.has(chunk.trackIndex)) {
|
|
@@ -1105,8 +1156,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1105
1156
|
// For audio codecs like opus/mp3 that don't need init data, this works fine
|
|
1106
1157
|
// For AAC, the description should come from track.init or the server will send INIT
|
|
1107
1158
|
const initData = pipeline.track.init ? str2bin(pipeline.track.init) : new Uint8Array(0);
|
|
1108
|
-
this.configurePipeline(chunk.trackIndex, initData).catch(err => {
|
|
1109
|
-
this.log(`Failed to configure track ${chunk.trackIndex}: ${err}`,
|
|
1159
|
+
this.configurePipeline(chunk.trackIndex, initData).catch((err) => {
|
|
1160
|
+
this.log(`Failed to configure track ${chunk.trackIndex}: ${err}`, "error");
|
|
1110
1161
|
});
|
|
1111
1162
|
return;
|
|
1112
1163
|
}
|
|
@@ -1131,10 +1182,10 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1131
1182
|
|
|
1132
1183
|
private sendChunkToWorker(chunk: RawChunk): void {
|
|
1133
1184
|
const msg: MainToWorkerMessage = {
|
|
1134
|
-
type:
|
|
1185
|
+
type: "receive",
|
|
1135
1186
|
idx: chunk.trackIndex,
|
|
1136
1187
|
chunk: {
|
|
1137
|
-
type: chunk.type ===
|
|
1188
|
+
type: chunk.type === "key" ? "key" : "delta",
|
|
1138
1189
|
timestamp: getPresentationTimestamp(chunk),
|
|
1139
1190
|
data: chunk.data,
|
|
1140
1191
|
},
|
|
@@ -1145,13 +1196,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1145
1196
|
}
|
|
1146
1197
|
|
|
1147
1198
|
private handleStop(): void {
|
|
1148
|
-
this.log(
|
|
1149
|
-
this.emit(
|
|
1199
|
+
this.log("Stream stopped");
|
|
1200
|
+
this.emit("ended", undefined);
|
|
1150
1201
|
}
|
|
1151
1202
|
|
|
1152
1203
|
private handleError(err: Error): void {
|
|
1153
|
-
this.log(`WebSocket error: ${err.message}`,
|
|
1154
|
-
this.emit(
|
|
1204
|
+
this.log(`WebSocket error: ${err.message}`, "error");
|
|
1205
|
+
this.emit("error", err);
|
|
1155
1206
|
}
|
|
1156
1207
|
|
|
1157
1208
|
// ============================================================================
|
|
@@ -1175,11 +1226,12 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1175
1226
|
|
|
1176
1227
|
// Create worker pipeline
|
|
1177
1228
|
await this.sendToWorker({
|
|
1178
|
-
type:
|
|
1229
|
+
type: "create",
|
|
1179
1230
|
idx: track.idx,
|
|
1180
1231
|
track,
|
|
1181
1232
|
opts: {
|
|
1182
|
-
optimizeForLatency: this.streamType ===
|
|
1233
|
+
optimizeForLatency: this.streamType === "live",
|
|
1234
|
+
payloadFormat: this.payloadFormat, // 'avcc' for ws/video/raw, 'annexb' for ws/video/h264
|
|
1183
1235
|
},
|
|
1184
1236
|
uid: this.workerUidCounter++,
|
|
1185
1237
|
});
|
|
@@ -1200,7 +1252,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1200
1252
|
|
|
1201
1253
|
await this.sendToWorker(
|
|
1202
1254
|
{
|
|
1203
|
-
type:
|
|
1255
|
+
type: "setwritable",
|
|
1204
1256
|
idx: track.idx,
|
|
1205
1257
|
writable: generator.writable,
|
|
1206
1258
|
uid: this.workerUidCounter++,
|
|
@@ -1212,12 +1264,12 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1212
1264
|
// Reference: rawws.js line 1012-1037
|
|
1213
1265
|
this.log(`Safari detected - using worker-based track generator for ${track.type}`);
|
|
1214
1266
|
|
|
1215
|
-
if (track.type ===
|
|
1267
|
+
if (track.type === "audio") {
|
|
1216
1268
|
// Safari audio: create generator on main thread, frames relayed from worker
|
|
1217
1269
|
// @ts-ignore - Safari has MediaStreamTrackGenerator for audio
|
|
1218
|
-
if (typeof MediaStreamTrackGenerator !==
|
|
1270
|
+
if (typeof MediaStreamTrackGenerator !== "undefined") {
|
|
1219
1271
|
// @ts-ignore
|
|
1220
|
-
const audioGen = new MediaStreamTrackGenerator({ kind:
|
|
1272
|
+
const audioGen = new MediaStreamTrackGenerator({ kind: "audio" });
|
|
1221
1273
|
pipeline.safariAudioGenerator = audioGen;
|
|
1222
1274
|
pipeline.safariAudioWriter = audioGen.writable.getWriter();
|
|
1223
1275
|
|
|
@@ -1230,13 +1282,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1230
1282
|
|
|
1231
1283
|
// Ask worker to create generator (video uses VideoTrackGenerator, audio sets up relay)
|
|
1232
1284
|
await this.sendToWorker({
|
|
1233
|
-
type:
|
|
1285
|
+
type: "creategenerator",
|
|
1234
1286
|
idx: track.idx,
|
|
1235
1287
|
uid: this.workerUidCounter++,
|
|
1236
1288
|
});
|
|
1237
1289
|
} else {
|
|
1238
1290
|
// Firefox/other: Use canvas/AudioWorklet polyfill
|
|
1239
|
-
pipeline.generator = createTrackGenerator(track.type as
|
|
1291
|
+
pipeline.generator = createTrackGenerator(track.type as "video" | "audio");
|
|
1240
1292
|
|
|
1241
1293
|
if (pipeline.generator.waitForInit) {
|
|
1242
1294
|
await pipeline.generator.waitForInit();
|
|
@@ -1244,7 +1296,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1244
1296
|
|
|
1245
1297
|
// For polyfill, writable stays on main thread
|
|
1246
1298
|
// Worker would need different architecture - for now, fall back to main thread decode
|
|
1247
|
-
this.log(
|
|
1299
|
+
this.log("Using MediaStreamTrackGenerator polyfill - main thread decode");
|
|
1248
1300
|
|
|
1249
1301
|
// Add track to stream directly
|
|
1250
1302
|
if (this.mediaStream && pipeline.generator) {
|
|
@@ -1259,14 +1311,18 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1259
1311
|
// However, if track.init is empty/undefined, the codec doesn't need init data
|
|
1260
1312
|
// and we can configure immediately (per rawws.js line 1239-1241).
|
|
1261
1313
|
// This applies to codecs like opus, mp3, vp8, vp9 that don't need init data.
|
|
1262
|
-
if (!track.init || track.init ===
|
|
1263
|
-
this.log(
|
|
1314
|
+
if (!track.init || track.init === "") {
|
|
1315
|
+
this.log(
|
|
1316
|
+
`Track ${track.idx} (${track.codec}) doesn't need init data, configuring immediately`
|
|
1317
|
+
);
|
|
1264
1318
|
await this.configurePipeline(track.idx, new Uint8Array(0));
|
|
1265
1319
|
} else {
|
|
1266
1320
|
// For codecs that need init data (H264, HEVC, AAC), we have two paths:
|
|
1267
1321
|
// 1. WebSocket sends INIT frame -> handleChunk triggers configurePipeline
|
|
1268
1322
|
// 2. First frame arrives without prior INIT -> handleChunk uses track.init
|
|
1269
|
-
this.log(
|
|
1323
|
+
this.log(
|
|
1324
|
+
`Track ${track.idx} (${track.codec}) has init data (${track.init.length} bytes), waiting for first frame`
|
|
1325
|
+
);
|
|
1270
1326
|
}
|
|
1271
1327
|
}
|
|
1272
1328
|
|
|
@@ -1281,7 +1337,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1281
1337
|
const headerCopy = new Uint8Array(header);
|
|
1282
1338
|
|
|
1283
1339
|
await this.sendToWorker({
|
|
1284
|
-
type:
|
|
1340
|
+
type: "configure",
|
|
1285
1341
|
idx,
|
|
1286
1342
|
header: headerCopy,
|
|
1287
1343
|
uid: this.workerUidCounter++,
|
|
@@ -1296,7 +1352,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1296
1352
|
// Find first keyframe to start from (can't decode deltas without reference)
|
|
1297
1353
|
let startIdx = 0;
|
|
1298
1354
|
for (let i = 0; i < queued.length; i++) {
|
|
1299
|
-
if (queued[i].type ===
|
|
1355
|
+
if (queued[i].type === "key") {
|
|
1300
1356
|
startIdx = i;
|
|
1301
1357
|
break;
|
|
1302
1358
|
}
|
|
@@ -1319,7 +1375,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1319
1375
|
|
|
1320
1376
|
// Close worker pipeline
|
|
1321
1377
|
await this.sendToWorker({
|
|
1322
|
-
type:
|
|
1378
|
+
type: "close",
|
|
1323
1379
|
idx,
|
|
1324
1380
|
waitEmpty,
|
|
1325
1381
|
uid: this.workerUidCounter++,
|
|
@@ -1342,8 +1398,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1342
1398
|
this._isPaused = false;
|
|
1343
1399
|
this.wsController?.play();
|
|
1344
1400
|
this.sendToWorker({
|
|
1345
|
-
type:
|
|
1346
|
-
action:
|
|
1401
|
+
type: "frametiming",
|
|
1402
|
+
action: "setPaused",
|
|
1347
1403
|
paused: false,
|
|
1348
1404
|
uid: this.workerUidCounter++,
|
|
1349
1405
|
});
|
|
@@ -1354,8 +1410,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1354
1410
|
this._isPaused = true;
|
|
1355
1411
|
this.wsController?.hold();
|
|
1356
1412
|
this.sendToWorker({
|
|
1357
|
-
type:
|
|
1358
|
-
action:
|
|
1413
|
+
type: "frametiming",
|
|
1414
|
+
action: "setPaused",
|
|
1359
1415
|
paused: true,
|
|
1360
1416
|
uid: this.workerUidCounter++,
|
|
1361
1417
|
});
|
|
@@ -1380,17 +1436,21 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1380
1436
|
|
|
1381
1437
|
this._pendingStepPause = false;
|
|
1382
1438
|
this._suppressPlayPauseSync = false;
|
|
1383
|
-
try {
|
|
1439
|
+
try {
|
|
1440
|
+
this.videoElement.pause();
|
|
1441
|
+
} catch {}
|
|
1384
1442
|
}
|
|
1385
1443
|
|
|
1386
1444
|
frameStep(direction: -1 | 1, _seconds?: number): void {
|
|
1387
1445
|
if (!this._isPaused) return;
|
|
1388
1446
|
if (!this.videoElement) return;
|
|
1389
|
-
this.log(
|
|
1447
|
+
this.log(
|
|
1448
|
+
`Frame step requested dir=${direction} paused=${this._isPaused} videoPaused=${this.videoElement.paused}`
|
|
1449
|
+
);
|
|
1390
1450
|
// Ensure worker is paused (in case pause didn't flow through)
|
|
1391
1451
|
this.sendToWorker({
|
|
1392
|
-
type:
|
|
1393
|
-
action:
|
|
1452
|
+
type: "frametiming",
|
|
1453
|
+
action: "setPaused",
|
|
1394
1454
|
paused: true,
|
|
1395
1455
|
uid: this.workerUidCounter++,
|
|
1396
1456
|
}).catch(() => {});
|
|
@@ -1403,19 +1463,19 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1403
1463
|
this._pendingStepPause = true;
|
|
1404
1464
|
try {
|
|
1405
1465
|
const maybePromise = video.play();
|
|
1406
|
-
if (maybePromise && typeof (maybePromise as Promise<void>).catch ===
|
|
1466
|
+
if (maybePromise && typeof (maybePromise as Promise<void>).catch === "function") {
|
|
1407
1467
|
(maybePromise as Promise<void>).catch(() => {});
|
|
1408
1468
|
}
|
|
1409
1469
|
} catch {}
|
|
1410
1470
|
|
|
1411
|
-
if (
|
|
1471
|
+
if ("requestVideoFrameCallback" in video) {
|
|
1412
1472
|
(video as any).requestVideoFrameCallback(() => this.finishStepPause());
|
|
1413
1473
|
}
|
|
1414
1474
|
// Failsafe: avoid staying in suppressed state if no frame is delivered
|
|
1415
1475
|
this._stepPauseTimeout = setTimeout(() => this.finishStepPause(), 200);
|
|
1416
1476
|
}
|
|
1417
1477
|
this.sendToWorker({
|
|
1418
|
-
type:
|
|
1478
|
+
type: "framestep",
|
|
1419
1479
|
direction,
|
|
1420
1480
|
uid: this.workerUidCounter++,
|
|
1421
1481
|
});
|
|
@@ -1429,11 +1489,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1429
1489
|
|
|
1430
1490
|
// Optimistically update current time for immediate UI feedback
|
|
1431
1491
|
this._currentTime = time;
|
|
1432
|
-
this.emit(
|
|
1492
|
+
this.emit("timeupdate", this._currentTime);
|
|
1433
1493
|
|
|
1434
1494
|
// Flush worker queues
|
|
1435
1495
|
this.sendToWorker({
|
|
1436
|
-
type:
|
|
1496
|
+
type: "seek",
|
|
1437
1497
|
seekTime: timeMs,
|
|
1438
1498
|
uid: this.workerUidCounter++,
|
|
1439
1499
|
});
|
|
@@ -1448,8 +1508,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1448
1508
|
if (this.syncController?.isSeekActive(seekId)) {
|
|
1449
1509
|
this.syncController.completeSeek(seekId);
|
|
1450
1510
|
this.sendToWorker({
|
|
1451
|
-
type:
|
|
1452
|
-
action:
|
|
1511
|
+
type: "frametiming",
|
|
1512
|
+
action: "reset",
|
|
1453
1513
|
uid: this.workerUidCounter++,
|
|
1454
1514
|
});
|
|
1455
1515
|
}
|
|
@@ -1465,17 +1525,17 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1465
1525
|
}
|
|
1466
1526
|
|
|
1467
1527
|
isLive(): boolean {
|
|
1468
|
-
return this.streamType ===
|
|
1528
|
+
return this.streamType === "live";
|
|
1469
1529
|
}
|
|
1470
1530
|
|
|
1471
1531
|
jumpToLive(): void {
|
|
1472
|
-
if (this.streamType ===
|
|
1532
|
+
if (this.streamType === "live" && this.wsController) {
|
|
1473
1533
|
// For WebCodecs live, request fresh data from live edge
|
|
1474
1534
|
// Send fast_forward to request 5 seconds of new data
|
|
1475
1535
|
// Reference: rawws.js live catchup sends fast_forward
|
|
1476
1536
|
const desiredBuffer = this.syncController?.getDesiredBuffer() ?? 2000;
|
|
1477
1537
|
this.wsController.send({
|
|
1478
|
-
type:
|
|
1538
|
+
type: "fast_forward",
|
|
1479
1539
|
ff_add: 5000, // Request 5 seconds ahead
|
|
1480
1540
|
});
|
|
1481
1541
|
|
|
@@ -1485,7 +1545,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1485
1545
|
this.wsController.seek(serverTime * 1000, desiredBuffer);
|
|
1486
1546
|
}
|
|
1487
1547
|
|
|
1488
|
-
this.log(
|
|
1548
|
+
this.log("Jump to live: requested fresh data from server");
|
|
1489
1549
|
}
|
|
1490
1550
|
}
|
|
1491
1551
|
|
|
@@ -1536,7 +1596,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1536
1596
|
return createTimeRanges([]);
|
|
1537
1597
|
}
|
|
1538
1598
|
const start = this._currentTime;
|
|
1539
|
-
const end = start +
|
|
1599
|
+
const end = start + this._bufferMs / 1000;
|
|
1540
1600
|
return createTimeRanges([[start, end]]);
|
|
1541
1601
|
}
|
|
1542
1602
|
|
|
@@ -1580,7 +1640,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1580
1640
|
if (!this.videoElement) return;
|
|
1581
1641
|
|
|
1582
1642
|
// Check if requestVideoFrameCallback is available
|
|
1583
|
-
if (
|
|
1643
|
+
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
|
|
1584
1644
|
const callback = (_now: DOMHighResTimeStamp, metadata: VideoFrameCallbackMetadata) => {
|
|
1585
1645
|
if (this.isDestroyed || !this.videoElement) return;
|
|
1586
1646
|
|
|
@@ -1591,10 +1651,10 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1591
1651
|
};
|
|
1592
1652
|
|
|
1593
1653
|
this._frameCallbackId = (this.videoElement as any).requestVideoFrameCallback(callback);
|
|
1594
|
-
this.log(
|
|
1654
|
+
this.log("requestVideoFrameCallback enabled for accurate frame timing");
|
|
1595
1655
|
} else {
|
|
1596
1656
|
// Fallback: Use video element's currentTime directly
|
|
1597
|
-
this.log(
|
|
1657
|
+
this.log("requestVideoFrameCallback not available, using fallback timing");
|
|
1598
1658
|
}
|
|
1599
1659
|
}
|
|
1600
1660
|
|
|
@@ -1613,7 +1673,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1613
1673
|
}
|
|
1614
1674
|
|
|
1615
1675
|
// Emit timeupdate event
|
|
1616
|
-
this.emit(
|
|
1676
|
+
this.emit("timeupdate", this._currentTime);
|
|
1617
1677
|
|
|
1618
1678
|
// Update frame stats
|
|
1619
1679
|
this._framesDecoded = metadata.presentedFrames;
|
|
@@ -1624,7 +1684,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1624
1684
|
*/
|
|
1625
1685
|
private cancelFrameCallback(): void {
|
|
1626
1686
|
if (this._frameCallbackId !== null && this.videoElement) {
|
|
1627
|
-
if (
|
|
1687
|
+
if ("cancelVideoFrameCallback" in HTMLVideoElement.prototype) {
|
|
1628
1688
|
(this.videoElement as any).cancelVideoFrameCallback(this._frameCallbackId);
|
|
1629
1689
|
}
|
|
1630
1690
|
this._frameCallbackId = null;
|
|
@@ -1635,16 +1695,16 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1635
1695
|
// Logging
|
|
1636
1696
|
// ============================================================================
|
|
1637
1697
|
|
|
1638
|
-
private log(message: string, level:
|
|
1639
|
-
if (!this.debugging && level ===
|
|
1698
|
+
private log(message: string, level: "info" | "warn" | "error" = "info"): void {
|
|
1699
|
+
if (!this.debugging && level === "info") return;
|
|
1640
1700
|
console[level](`[WebCodecs] ${message}`);
|
|
1641
1701
|
}
|
|
1642
1702
|
}
|
|
1643
1703
|
|
|
1644
1704
|
// Export for direct use
|
|
1645
|
-
export { WebSocketController } from
|
|
1646
|
-
export { SyncController } from
|
|
1647
|
-
export { JitterTracker, MultiTrackJitterTracker } from
|
|
1648
|
-
export { getLatencyProfile, mergeLatencyProfile, LATENCY_PROFILES } from
|
|
1649
|
-
export { parseRawChunk, RawChunkParser } from
|
|
1650
|
-
export * from
|
|
1705
|
+
export { WebSocketController } from "./WebSocketController";
|
|
1706
|
+
export { SyncController } from "./SyncController";
|
|
1707
|
+
export { JitterTracker, MultiTrackJitterTracker } from "./JitterBuffer";
|
|
1708
|
+
export { getLatencyProfile, mergeLatencyProfile, LATENCY_PROFILES } from "./LatencyProfiles";
|
|
1709
|
+
export { parseRawChunk, RawChunkParser } from "./RawChunkParser";
|
|
1710
|
+
export * from "./types";
|