@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.
Files changed (82) hide show
  1. package/README.md +21 -6
  2. package/dist/cjs/index.js +792 -146
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/esm/index.js +792 -146
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/player.css +185 -373
  7. package/dist/types/core/GatewayClient.d.ts +3 -4
  8. package/dist/types/core/InteractionController.d.ts +12 -0
  9. package/dist/types/core/MetaTrackManager.d.ts +1 -1
  10. package/dist/types/core/PlayerController.d.ts +18 -2
  11. package/dist/types/core/PlayerInterface.d.ts +10 -0
  12. package/dist/types/core/SeekingUtils.d.ts +3 -1
  13. package/dist/types/core/StreamStateClient.d.ts +1 -1
  14. package/dist/types/players/HlsJsPlayer.d.ts +8 -0
  15. package/dist/types/players/MewsWsPlayer/index.d.ts +1 -1
  16. package/dist/types/players/VideoJsPlayer.d.ts +12 -4
  17. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +1 -1
  18. package/dist/types/players/WebCodecsPlayer/index.d.ts +11 -0
  19. package/dist/types/players/WebCodecsPlayer/types.d.ts +25 -3
  20. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +20 -2
  21. package/dist/types/types.d.ts +32 -1
  22. package/dist/types/vanilla/FrameWorksPlayer.d.ts +5 -5
  23. package/dist/types/vanilla/index.d.ts +3 -3
  24. package/dist/workers/decoder.worker.js +183 -6
  25. package/dist/workers/decoder.worker.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/core/ABRController.ts +38 -36
  28. package/src/core/CodecUtils.ts +50 -47
  29. package/src/core/Disposable.ts +4 -4
  30. package/src/core/EventEmitter.ts +1 -1
  31. package/src/core/GatewayClient.ts +48 -48
  32. package/src/core/InteractionController.ts +89 -82
  33. package/src/core/LiveDurationProxy.ts +14 -16
  34. package/src/core/MetaTrackManager.ts +74 -66
  35. package/src/core/MistReporter.ts +72 -45
  36. package/src/core/MistSignaling.ts +59 -56
  37. package/src/core/PlayerController.ts +724 -375
  38. package/src/core/PlayerInterface.ts +89 -59
  39. package/src/core/PlayerManager.ts +118 -123
  40. package/src/core/PlayerRegistry.ts +59 -42
  41. package/src/core/QualityMonitor.ts +38 -31
  42. package/src/core/ScreenWakeLockManager.ts +8 -9
  43. package/src/core/SeekingUtils.ts +31 -22
  44. package/src/core/StreamStateClient.ts +75 -69
  45. package/src/core/SubtitleManager.ts +25 -23
  46. package/src/core/TelemetryReporter.ts +34 -31
  47. package/src/core/TimeFormat.ts +13 -17
  48. package/src/core/TimerManager.ts +25 -9
  49. package/src/core/UrlUtils.ts +20 -17
  50. package/src/core/detector.ts +44 -44
  51. package/src/core/index.ts +57 -48
  52. package/src/core/scorer.ts +137 -138
  53. package/src/core/selector.ts +2 -6
  54. package/src/global.d.ts +1 -1
  55. package/src/index.ts +46 -35
  56. package/src/players/DashJsPlayer.ts +175 -114
  57. package/src/players/HlsJsPlayer.ts +154 -76
  58. package/src/players/MewsWsPlayer/SourceBufferManager.ts +44 -39
  59. package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -10
  60. package/src/players/MewsWsPlayer/index.ts +196 -154
  61. package/src/players/MewsWsPlayer/types.ts +21 -21
  62. package/src/players/MistPlayer.ts +46 -27
  63. package/src/players/MistWebRTCPlayer/index.ts +175 -129
  64. package/src/players/NativePlayer.ts +203 -143
  65. package/src/players/VideoJsPlayer.ts +200 -146
  66. package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
  67. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
  68. package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
  69. package/src/players/WebCodecsPlayer/SyncController.ts +46 -55
  70. package/src/players/WebCodecsPlayer/WebSocketController.ts +67 -69
  71. package/src/players/WebCodecsPlayer/index.ts +280 -220
  72. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
  73. package/src/players/WebCodecsPlayer/types.ts +81 -53
  74. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +255 -192
  75. package/src/players/WebCodecsPlayer/worker/types.ts +33 -29
  76. package/src/players/index.ts +8 -8
  77. package/src/styles/animations.css +2 -1
  78. package/src/styles/player.css +182 -356
  79. package/src/styles/tailwind.css +473 -159
  80. package/src/types.ts +75 -33
  81. package/src/vanilla/FrameWorksPlayer.ts +34 -19
  82. 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 '../../core/PlayerInterface';
17
+ import { BasePlayer } from "../../core/PlayerInterface";
18
18
  import type {
19
19
  StreamSource,
20
20
  StreamInfo,
21
21
  PlayerOptions,
22
22
  PlayerCapability,
23
- } from '../../core/PlayerInterface';
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 './types';
36
- import { WebSocketController } from './WebSocketController';
37
- import { SyncController } from './SyncController';
38
- import { getPresentationTimestamp, isInitData } from './RawChunkParser';
39
- import { getLatencyProfile, mergeLatencyProfile, selectDefaultProfile } from './LatencyProfiles';
40
- import { createTrackGenerator, hasNativeMediaStreamTrackGenerator } from './polyfills/MediaStreamTrackGenerator';
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 === 'undefined') return false;
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('Index out of bounds');
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('Index out of bounds');
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: 'WebCodecs Player',
115
- shortname: 'webcodecs',
116
+ name: "WebCodecs Player",
117
+ shortname: "webcodecs",
116
118
  priority: 0, // Highest priority - lowest latency option
117
- // Raw WebSocket (12-byte header + AVCC NAL units) - NOT MP4-muxed
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
- 'ws/video/raw', 'wss/video/raw', // Raw codec frames (audio + video)
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: 'live' | 'vod' = 'live';
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: { codec: string; codecstring?: string; init?: string }): string {
167
- const codecStr = track.codecstring ?? track.codec?.toLowerCase() ?? '';
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 = init.length > 0 ? `${init.length}_${init.charCodeAt(0)}_${init.charCodeAt(init.length - 1)}` : '';
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 ?? '').toLowerCase();
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 'video': {
215
+ case "video": {
201
216
  // Special handling for JPEG - uses ImageDecoder
202
- if (track.codec === 'JPEG') {
203
- if (!('ImageDecoder' in window)) {
204
- result = { supported: false, config: { codec: 'image/jpeg' } };
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('image/jpeg');
208
- result = { supported: isSupported, config: { codec: 'image/jpeg' } };
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
- result = await VideoDecoder.isConfigSupported(config as VideoDecoderConfig);
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 'audio': {
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
- result = await AudioDecoder.isConfigSupported(config as AudioDecoderConfig);
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 === 'video' || t.type === 'audio')
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(`[WebCodecs] Track ${track.idx} (${track.type} ${track.codec}): ${supported ? 'supported' : 'UNSUPPORTED'}`);
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 (!('WebSocket' in window)) {
292
+ if (!("WebSocket" in window)) {
274
293
  return false;
275
294
  }
276
- if (!('Worker' in window)) {
295
+ if (!("Worker" in window)) {
277
296
  return false;
278
297
  }
279
- if (!('VideoDecoder' in window) || !('AudioDecoder' in window)) {
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/, 'http'), location.href);
286
- if (location.protocol === 'https:' && sourceUrl.protocol === 'http:') {
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 === 'video' || track.type === 'audio') {
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 === 'meta' && track.codec === 'subtitle') {
327
+ } else if (track.type === "meta" && track.codec === "subtitle") {
309
328
  // Subtitles supported via text track
310
- playableTracks['subtitle'] = true;
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('fw-player-container');
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['type'],
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 === 'live' ? 'live' : 'vod';
410
+ this.streamType = (source as any).type === "live" ? "live" : "vod";
380
411
 
381
412
  // Select latency profile
382
- const profileName = wcOptions.latencyProfile ?? selectDefaultProfile(this.streamType === 'live');
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('video');
389
- video.classList.add('fw-player-video');
390
- video.setAttribute('playsinline', '');
391
- video.setAttribute('crossorigin', 'anonymous');
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 !== 'live') video.loop = true;
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: 'frametiming',
408
- action: 'setPaused',
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: 'frametiming',
418
- action: 'setPaused',
449
+ type: "frametiming",
450
+ action: "setPaused",
419
451
  paused: true,
420
452
  uid: this.workerUidCounter++,
421
453
  }).catch(() => {});
422
454
  };
423
- video.addEventListener('play', this._onVideoPlay);
424
- video.addEventListener('pause', this._onVideoPause);
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 === 'live',
468
+ isLive: this.streamType === "live",
437
469
  onSpeedChange: (main, tweak) => {
438
470
  this.sendToWorker({
439
- type: 'frametiming',
440
- action: 'setSpeed',
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('Validating track codecs with isConfigSupported()...');
500
+ this.log("Validating track codecs with isConfigSupported()...");
469
501
 
470
502
  for (const track of streamInfo.meta.tracks) {
471
- if (track.type === 'video' || track.type === 'audio') {
503
+ if (track.type === "video" || track.type === "audio") {
472
504
  const trackInfo: TrackInfo = {
473
505
  idx: track.idx ?? 0,
474
- type: track.type as 'video' | 'audio',
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 === 'audio') {
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`, 'warn');
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('No validated codecs, using default codec list');
504
- ['AAC', 'MP3', 'opus', 'FLAC', 'AC3'].forEach(c => supportedAudioCodecs.add(c));
505
- ['H264', 'HEVC', 'VP8', 'VP9', 'AV1', 'JPEG'].forEach(c => supportedVideoCodecs.add(c));
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
- Array.from(supportedAudioCodecs), // Audio codecs (position 0)
513
- Array.from(supportedVideoCodecs), // Video codecs (position 1)
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(`Requesting codecs: audio=[${supportedCombinations[0][0].join(', ')}], video=[${supportedCombinations[0][1].join(', ')}]`);
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}`, 'error');
523
- this.emit('error', err instanceof Error ? err : new Error(String(err)));
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 === 'video' || track.type === 'audio') {
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('Destroying WebCodecs player');
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('play', this._onVideoPlay);
624
+ this.videoElement.removeEventListener("play", this._onVideoPlay);
589
625
  this._onVideoPlay = undefined;
590
626
  }
591
627
  if (this._onVideoPause) {
592
- this.videoElement.removeEventListener('pause', this._onVideoPause);
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: 'module' });
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('error', onError);
632
- worker.removeEventListener('message', onMessage);
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 || 'Worker failed to load'));
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('error', onError);
653
- worker.addEventListener('message', onMessage);
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('../workers/decoder.worker.js', import.meta.url).href);
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}`, 'warn');
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
- 'Failed to initialize WebCodecs worker. ' +
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 ?? 'unknown error'}`, 'error');
699
- this.emit('error', new Error(`Worker error: ${err?.message ?? 'unknown'}`));
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: 'debugging',
705
- value: this.verboseDebugging ? 'verbose' : this.debugging,
737
+ type: "debugging",
738
+ value: this.verboseDebugging ? "verbose" : this.debugging,
706
739
  uid: this.workerUidCounter++,
707
740
  });
708
741
  }
709
742
 
710
- private sendToWorker(msg: MainToWorkerMessage & { uid: number }, transfer?: Transferable[]): Promise<WorkerToMainMessage> {
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('Player destroyed'));
751
+ reject(new Error("Player destroyed"));
716
752
  return;
717
753
  }
718
754
  if (!this.worker) {
719
- reject(new Error('Worker not initialized'));
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 === 'ack' && response.status === 'error') {
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 'addtrack': {
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 'removetrack': {
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 'setplaybackrate': {
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 'sendevent': {
782
- if (msg.kind === 'timeupdate') {
817
+ case "sendevent": {
818
+ if (msg.kind === "timeupdate") {
783
819
  if (this._pendingStepPause) {
784
820
  this.finishStepPause();
785
821
  }
786
- if (typeof msg.time === 'number' && Number.isFinite(msg.time)) {
822
+ if (typeof msg.time === "number" && Number.isFinite(msg.time)) {
787
823
  this._currentTime = msg.time;
788
- this.emit('timeupdate', this._currentTime);
824
+ this.emit("timeupdate", this._currentTime);
789
825
  } else if (this.videoElement) {
790
- this.emit('timeupdate', this.videoElement.currentTime);
826
+ this.emit("timeupdate", this.videoElement.currentTime);
791
827
  }
792
- } else if (msg.kind === 'error') {
793
- this.emit('error', new Error(msg.message ?? 'Unknown error'));
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 'writeframe': {
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.write(frame).then(() => {
806
- this.worker?.postMessage({
807
- type: 'writeframe',
808
- idx: msg.idx,
809
- uid: frameUid,
810
- status: 'ok',
811
- });
812
- }).catch((err: Error) => {
813
- this.worker?.postMessage({
814
- type: 'writeframe',
815
- idx: msg.idx,
816
- uid: frameUid,
817
- status: 'error',
818
- error: err.message,
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: 'writeframe',
862
+ type: "writeframe",
824
863
  idx: msg.idx,
825
864
  uid: msg.uid,
826
- status: 'error',
827
- error: 'Pipeline not active or no audio writer',
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 'log': {
872
+ case "log": {
834
873
  if (this.debugging) {
835
- const level = (msg as any).level ?? 'info';
836
- const logFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
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 'stats': {
882
+ case "stats": {
843
883
  // Could emit stats for monitoring
844
884
  break;
845
885
  }
846
886
 
847
- case 'closed': {
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('codecdata', (msg) => this.handleCodecData(msg));
862
- this.wsController.on('info', (msg) => this.handleInfo(msg));
863
- this.wsController.on('ontime', (msg) => this.handleOnTime(msg));
864
- this.wsController.on('tracks', (tracks) => this.handleTracksChange(tracks));
865
- this.wsController.on('chunk', (chunk) => this.handleChunk(chunk));
866
- this.wsController.on('stop', () => this.handleStop());
867
- this.wsController.on('error', (err) => this.handleError(err));
868
- this.wsController.on('statechange', (state) => {
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 === 'error') {
871
- this.emit('error', new Error('WebSocket connection failed'));
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 ?? []; // Array of track indices (numbers), NOT TrackInfo
879
- this.log(`Received codec data: codecs=[${codecs.join(', ') || 'none'}], tracks=[${trackIndices.join(', ') || 'none'}]`);
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('No playable codecs/tracks selected by server', 'warn');
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) ? 'video' :
903
- codec.match(/^(AAC|MP3|opus|FLAC|AC3|pcm)/i) ? 'audio' : 'meta',
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 === 'video' || track.type === 'audio')) {
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('Received stream info');
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 [name, track] of Object.entries(tracksObj)) {
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 === 'video' || track.type === 'audio') {
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 === 'video' || track.type === 'audio')) {
991
- this.log(`Creating pipeline from on_time for track ${track.idx} (${track.type} ${track.codec})`);
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 === 'video' || track.type === 'audio') {
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`, 'warn');
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 === 'video' || track.type === 'audio') {
1061
- this.log(`Creating pipeline for discovered track ${track.idx} (${track.type} ${track.codec})`);
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 === 'audio';
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 === 'key';
1142
+ const shouldConfigure = isAudioTrack || chunk.type === "key";
1094
1143
 
1095
1144
  if (shouldConfigure) {
1096
- this.log(`Received ${chunk.type.toUpperCase()} frame for unconfigured ${pipeline.track.type} track ${chunk.trackIndex}, configuring`);
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}`, 'error');
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: 'receive',
1185
+ type: "receive",
1135
1186
  idx: chunk.trackIndex,
1136
1187
  chunk: {
1137
- type: chunk.type === 'key' ? 'key' : 'delta',
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('Stream stopped');
1149
- this.emit('ended', undefined);
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}`, 'error');
1154
- this.emit('error', err);
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: 'create',
1229
+ type: "create",
1179
1230
  idx: track.idx,
1180
1231
  track,
1181
1232
  opts: {
1182
- optimizeForLatency: this.streamType === 'live',
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: 'setwritable',
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 === 'audio') {
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 !== 'undefined') {
1270
+ if (typeof MediaStreamTrackGenerator !== "undefined") {
1219
1271
  // @ts-ignore
1220
- const audioGen = new MediaStreamTrackGenerator({ kind: 'audio' });
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: 'creategenerator',
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 'video' | 'audio');
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('Using MediaStreamTrackGenerator polyfill - main thread decode');
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(`Track ${track.idx} (${track.codec}) doesn't need init data, configuring immediately`);
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(`Track ${track.idx} (${track.codec}) has init data (${track.init.length} bytes), waiting for first frame`);
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: 'configure',
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 === 'key') {
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: 'close',
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: 'frametiming',
1346
- action: 'setPaused',
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: 'frametiming',
1358
- action: 'setPaused',
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 { this.videoElement.pause(); } catch {}
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(`Frame step requested dir=${direction} paused=${this._isPaused} videoPaused=${this.videoElement.paused}`);
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: 'frametiming',
1393
- action: 'setPaused',
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 === 'function') {
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 ('requestVideoFrameCallback' in video) {
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: 'framestep',
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('timeupdate', this._currentTime);
1492
+ this.emit("timeupdate", this._currentTime);
1433
1493
 
1434
1494
  // Flush worker queues
1435
1495
  this.sendToWorker({
1436
- type: 'seek',
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: 'frametiming',
1452
- action: 'reset',
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 === 'live';
1528
+ return this.streamType === "live";
1469
1529
  }
1470
1530
 
1471
1531
  jumpToLive(): void {
1472
- if (this.streamType === 'live' && this.wsController) {
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: 'fast_forward',
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('Jump to live: requested fresh data from server');
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 + (this._bufferMs / 1000);
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 ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
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('requestVideoFrameCallback enabled for accurate frame timing');
1654
+ this.log("requestVideoFrameCallback enabled for accurate frame timing");
1595
1655
  } else {
1596
1656
  // Fallback: Use video element's currentTime directly
1597
- this.log('requestVideoFrameCallback not available, using fallback timing');
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('timeupdate', this._currentTime);
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 ('cancelVideoFrameCallback' in HTMLVideoElement.prototype) {
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: 'info' | 'warn' | 'error' = 'info'): void {
1639
- if (!this.debugging && level === 'info') return;
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 './WebSocketController';
1646
- export { SyncController } from './SyncController';
1647
- export { JitterTracker, MultiTrackJitterTracker } from './JitterBuffer';
1648
- export { getLatencyProfile, mergeLatencyProfile, LATENCY_PROFILES } from './LatencyProfiles';
1649
- export { parseRawChunk, RawChunkParser } from './RawChunkParser';
1650
- export * from './types';
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";