@livepeer-frameworks/player-core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +11 -9
  2. package/dist/player.css +182 -42
  3. package/package.json +1 -1
  4. package/src/core/ABRController.ts +38 -36
  5. package/src/core/CodecUtils.ts +49 -46
  6. package/src/core/Disposable.ts +4 -4
  7. package/src/core/EventEmitter.ts +1 -1
  8. package/src/core/GatewayClient.ts +41 -39
  9. package/src/core/InteractionController.ts +89 -82
  10. package/src/core/LiveDurationProxy.ts +14 -15
  11. package/src/core/MetaTrackManager.ts +73 -65
  12. package/src/core/MistReporter.ts +72 -45
  13. package/src/core/MistSignaling.ts +59 -56
  14. package/src/core/PlayerController.ts +527 -384
  15. package/src/core/PlayerInterface.ts +83 -59
  16. package/src/core/PlayerManager.ts +79 -133
  17. package/src/core/PlayerRegistry.ts +59 -42
  18. package/src/core/QualityMonitor.ts +38 -31
  19. package/src/core/ScreenWakeLockManager.ts +8 -9
  20. package/src/core/SeekingUtils.ts +31 -22
  21. package/src/core/StreamStateClient.ts +74 -68
  22. package/src/core/SubtitleManager.ts +24 -22
  23. package/src/core/TelemetryReporter.ts +34 -31
  24. package/src/core/TimeFormat.ts +13 -17
  25. package/src/core/TimerManager.ts +24 -8
  26. package/src/core/UrlUtils.ts +20 -17
  27. package/src/core/detector.ts +44 -44
  28. package/src/core/index.ts +57 -48
  29. package/src/core/scorer.ts +136 -141
  30. package/src/core/selector.ts +2 -6
  31. package/src/global.d.ts +1 -1
  32. package/src/index.ts +46 -35
  33. package/src/players/DashJsPlayer.ts +164 -115
  34. package/src/players/HlsJsPlayer.ts +132 -78
  35. package/src/players/MewsWsPlayer/SourceBufferManager.ts +41 -36
  36. package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -9
  37. package/src/players/MewsWsPlayer/index.ts +192 -152
  38. package/src/players/MewsWsPlayer/types.ts +21 -21
  39. package/src/players/MistPlayer.ts +45 -26
  40. package/src/players/MistWebRTCPlayer/index.ts +175 -129
  41. package/src/players/NativePlayer.ts +203 -143
  42. package/src/players/VideoJsPlayer.ts +170 -118
  43. package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
  44. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
  45. package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
  46. package/src/players/WebCodecsPlayer/SyncController.ts +45 -53
  47. package/src/players/WebCodecsPlayer/WebSocketController.ts +66 -68
  48. package/src/players/WebCodecsPlayer/index.ts +263 -221
  49. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
  50. package/src/players/WebCodecsPlayer/types.ts +56 -56
  51. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +238 -182
  52. package/src/players/WebCodecsPlayer/worker/types.ts +31 -31
  53. package/src/players/index.ts +8 -8
  54. package/src/styles/animations.css +2 -1
  55. package/src/styles/player.css +182 -42
  56. package/src/styles/tailwind.css +473 -159
  57. package/src/types.ts +43 -43
  58. package/src/vanilla/FrameWorksPlayer.ts +29 -14
  59. package/src/vanilla/index.ts +4 -4
@@ -40,61 +40,61 @@ export function translateCodec(track: TrackInfo): string {
40
40
  }
41
41
 
42
42
  // Audio codecs
43
- if (track.type === 'audio') {
43
+ if (track.type === "audio") {
44
44
  switch (codec) {
45
- case 'AAC':
46
- case 'MP4A':
47
- return 'mp4a.40.2'; // AAC-LC
48
- case 'MP3':
49
- return 'mp4a.40.34'; // MP3 in MP4 container
50
- case 'AC3':
51
- case 'AC-3':
52
- return 'ac-3';
53
- case 'EAC3':
54
- case 'EC3':
55
- case 'E-AC3':
56
- case 'EC-3':
57
- return 'ec-3';
58
- case 'OPUS':
59
- return 'opus';
60
- case 'VORBIS':
61
- return 'vorbis';
62
- case 'FLAC':
63
- return 'flac';
64
- case 'PCM':
65
- case 'PCMS16LE':
66
- return 'pcm';
45
+ case "AAC":
46
+ case "MP4A":
47
+ return "mp4a.40.2"; // AAC-LC
48
+ case "MP3":
49
+ return "mp4a.40.34"; // MP3 in MP4 container
50
+ case "AC3":
51
+ case "AC-3":
52
+ return "ac-3";
53
+ case "EAC3":
54
+ case "EC3":
55
+ case "E-AC3":
56
+ case "EC-3":
57
+ return "ec-3";
58
+ case "OPUS":
59
+ return "opus";
60
+ case "VORBIS":
61
+ return "vorbis";
62
+ case "FLAC":
63
+ return "flac";
64
+ case "PCM":
65
+ case "PCMS16LE":
66
+ return "pcm";
67
67
  default:
68
68
  return codec.toLowerCase();
69
69
  }
70
70
  }
71
71
 
72
72
  // Video codecs
73
- if (track.type === 'video') {
73
+ if (track.type === "video") {
74
74
  switch (codec) {
75
- case 'H264':
76
- case 'AVC':
77
- case 'AVC1': {
75
+ case "H264":
76
+ case "AVC":
77
+ case "AVC1": {
78
78
  // Try to extract profile/level from init data
79
79
  const profileLevel = extractH264Profile(track.init);
80
- return profileLevel || 'avc1.42E01E'; // Default: Baseline Profile, Level 3.0
80
+ return profileLevel || "avc1.42E01E"; // Default: Baseline Profile, Level 3.0
81
81
  }
82
- case 'H265':
83
- case 'HEVC':
84
- case 'HEV1':
85
- case 'HVC1': {
82
+ case "H265":
83
+ case "HEVC":
84
+ case "HEV1":
85
+ case "HVC1": {
86
86
  // Try to extract profile/level from init data
87
87
  const profileLevel = extractHEVCProfile(track.init);
88
- return profileLevel || 'hev1.1.6.L93.B0'; // Default: Main Profile, Level 3.1
88
+ return profileLevel || "hev1.1.6.L93.B0"; // Default: Main Profile, Level 3.1
89
89
  }
90
- case 'VP8':
91
- return 'vp8';
92
- case 'VP9':
93
- return 'vp09.00.10.08'; // Profile 0, Level 1.0, 8-bit
94
- case 'AV1':
95
- return 'av01.0.01M.08'; // Main Profile, Level 2.1, 8-bit
96
- case 'THEORA':
97
- return 'theora';
90
+ case "VP8":
91
+ return "vp8";
92
+ case "VP9":
93
+ return "vp09.00.10.08"; // Profile 0, Level 1.0, 8-bit
94
+ case "AV1":
95
+ return "av01.0.01M.08"; // Main Profile, Level 2.1, 8-bit
96
+ case "THEORA":
97
+ return "theora";
98
98
  default:
99
99
  return codec.toLowerCase();
100
100
  }
@@ -199,7 +199,7 @@ function extractHEVCProfile(init?: string): string | null {
199
199
  * Convert byte to 2-digit hex string
200
200
  */
201
201
  function toHex(byte: number): string {
202
- return byte.toString(16).padStart(2, '0').toUpperCase();
202
+ return byte.toString(16).padStart(2, "0").toUpperCase();
203
203
  }
204
204
 
205
205
  /**
@@ -221,8 +221,8 @@ function base64ToBytes(base64: string): Uint8Array {
221
221
  * @param containerType - Container type (default: 'video/mp4')
222
222
  * @returns true if supported
223
223
  */
224
- export function isCodecSupported(codecString: string, containerType = 'video/mp4'): boolean {
225
- if (typeof MediaSource === 'undefined' || !MediaSource.isTypeSupported) {
224
+ export function isCodecSupported(codecString: string, containerType = "video/mp4"): boolean {
225
+ if (typeof MediaSource === "undefined" || !MediaSource.isTypeSupported) {
226
226
  return false;
227
227
  }
228
228
 
@@ -237,8 +237,11 @@ export function isCodecSupported(codecString: string, containerType = 'video/mp4
237
237
  * @param type - Track type to filter ('video' or 'audio')
238
238
  * @returns Best supported track or null
239
239
  */
240
- export function getBestSupportedTrack(tracks: TrackInfo[], type: 'video' | 'audio'): TrackInfo | null {
241
- const filteredTracks = tracks.filter(t => t.type === type);
240
+ export function getBestSupportedTrack(
241
+ tracks: TrackInfo[],
242
+ type: "video" | "audio"
243
+ ): TrackInfo | null {
244
+ const filteredTracks = tracks.filter((t) => t.type === type);
242
245
 
243
246
  for (const track of filteredTracks) {
244
247
  const codecString = translateCodec(track);
@@ -57,7 +57,7 @@ export abstract class BaseDisposable implements Disposable {
57
57
  * Throw if this object has been disposed.
58
58
  * Use at the start of methods that shouldn't run after disposal.
59
59
  */
60
- protected throwIfDisposed(operation: string = 'operation'): void {
60
+ protected throwIfDisposed(operation: string = "operation"): void {
61
61
  if (this._disposed) {
62
62
  throw new Error(`Cannot perform ${operation} on disposed object`);
63
63
  }
@@ -80,7 +80,7 @@ export function disposeAll(...disposables: (Disposable | null | undefined)[]): v
80
80
  try {
81
81
  d.dispose();
82
82
  } catch (err) {
83
- console.warn('[Disposable] Error during disposal:', err);
83
+ console.warn("[Disposable] Error during disposal:", err);
84
84
  }
85
85
  }
86
86
  }
@@ -104,13 +104,13 @@ export function createCompositeDisposable(
104
104
 
105
105
  for (const d of disposables) {
106
106
  try {
107
- if (typeof d === 'function') {
107
+ if (typeof d === "function") {
108
108
  d();
109
109
  } else if (d && !d.disposed) {
110
110
  d.dispose();
111
111
  }
112
112
  } catch (err) {
113
- console.warn('[CompositeDisposable] Error during disposal:', err);
113
+ console.warn("[CompositeDisposable] Error during disposal:", err);
114
114
  }
115
115
  }
116
116
  },
@@ -77,7 +77,7 @@ export class TypedEventEmitter<Events extends Record<string, any>> {
77
77
  * @param data - The event payload
78
78
  */
79
79
  protected emit<K extends keyof Events>(event: K, data: Events[K]): void {
80
- this.listeners.get(event)?.forEach(listener => {
80
+ this.listeners.get(event)?.forEach((listener) => {
81
81
  try {
82
82
  listener(data);
83
83
  } catch (e) {
@@ -5,14 +5,14 @@
5
5
  * Extracted from useViewerEndpoints.ts for use in headless core.
6
6
  */
7
7
 
8
- import { TypedEventEmitter } from './EventEmitter';
9
- import type { ContentEndpoints, ContentType } from '../types';
8
+ import { TypedEventEmitter } from "./EventEmitter";
9
+ import type { ContentEndpoints, ContentType } from "../types";
10
10
 
11
11
  // ============================================================================
12
12
  // Types
13
13
  // ============================================================================
14
14
 
15
- export type GatewayStatus = 'idle' | 'loading' | 'ready' | 'error';
15
+ export type GatewayStatus = "idle" | "loading" | "ready" | "error";
16
16
 
17
17
  export interface GatewayClientConfig {
18
18
  /** Gateway GraphQL endpoint URL */
@@ -45,10 +45,10 @@ const DEFAULT_INITIAL_DELAY_MS = 500;
45
45
  // F2: Cache TTL for resolved endpoints
46
46
  const DEFAULT_CACHE_TTL_MS = 10000;
47
47
  // F3: Circuit breaker constants
48
- const CIRCUIT_BREAKER_THRESHOLD = 5; // Open after 5 consecutive failures
49
- const CIRCUIT_BREAKER_TIMEOUT_MS = 30000; // Half-open after 30 seconds
48
+ const CIRCUIT_BREAKER_THRESHOLD = 5; // Open after 5 consecutive failures
49
+ const CIRCUIT_BREAKER_TIMEOUT_MS = 30000; // Half-open after 30 seconds
50
50
 
51
- type CircuitBreakerState = 'closed' | 'open' | 'half-open';
51
+ type CircuitBreakerState = "closed" | "open" | "half-open";
52
52
 
53
53
  const RESOLVE_VIEWER_QUERY = `
54
54
  query ResolveViewer($contentId: String!) {
@@ -80,7 +80,7 @@ async function fetchWithRetry(
80
80
  const response = await fetch(url, options);
81
81
  return response;
82
82
  } catch (e) {
83
- lastError = e instanceof Error ? e : new Error('Fetch failed');
83
+ lastError = e instanceof Error ? e : new Error("Fetch failed");
84
84
 
85
85
  // Don't retry on abort
86
86
  if (options.signal?.aborted) {
@@ -91,12 +91,12 @@ async function fetchWithRetry(
91
91
  if (attempt < maxRetries - 1) {
92
92
  const delay = initialDelay * Math.pow(2, attempt);
93
93
  console.warn(`[GatewayClient] Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms`);
94
- await new Promise(resolve => setTimeout(resolve, delay));
94
+ await new Promise((resolve) => setTimeout(resolve, delay));
95
95
  }
96
96
  }
97
97
  }
98
98
 
99
- throw lastError ?? new Error('Gateway unreachable after retries');
99
+ throw lastError ?? new Error("Gateway unreachable after retries");
100
100
  }
101
101
 
102
102
  // ============================================================================
@@ -121,7 +121,7 @@ async function fetchWithRetry(
121
121
  */
122
122
  export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
123
123
  private config: GatewayClientConfig;
124
- private status: GatewayStatus = 'idle';
124
+ private status: GatewayStatus = "idle";
125
125
  private endpoints: ContentEndpoints | null = null;
126
126
  private error: string | null = null;
127
127
  private abortController: AbortController | null = null;
@@ -133,7 +133,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
133
133
  private cacheTtlMs: number;
134
134
 
135
135
  // F3: Circuit breaker state
136
- private circuitState: CircuitBreakerState = 'closed';
136
+ private circuitState: CircuitBreakerState = "closed";
137
137
  private consecutiveFailures = 0;
138
138
  private circuitOpenedAt = 0;
139
139
 
@@ -159,7 +159,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
159
159
 
160
160
  // F3: Check circuit breaker
161
161
  if (!this.canAttemptRequest()) {
162
- throw new Error('Circuit breaker is open - too many recent failures');
162
+ throw new Error("Circuit breaker is open - too many recent failures");
163
163
  }
164
164
 
165
165
  // F2: Return in-flight request if one exists (deduplication)
@@ -214,18 +214,18 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
214
214
  */
215
215
  private canAttemptRequest(): boolean {
216
216
  switch (this.circuitState) {
217
- case 'closed':
217
+ case "closed":
218
218
  return true;
219
219
 
220
- case 'open':
220
+ case "open":
221
221
  // Check if enough time has passed to try half-open
222
222
  if (Date.now() - this.circuitOpenedAt >= CIRCUIT_BREAKER_TIMEOUT_MS) {
223
- this.circuitState = 'half-open';
223
+ this.circuitState = "half-open";
224
224
  return true;
225
225
  }
226
226
  return false;
227
227
 
228
- case 'half-open':
228
+ case "half-open":
229
229
  // Allow one request to test the circuit
230
230
  return true;
231
231
  }
@@ -236,7 +236,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
236
236
  */
237
237
  private onSuccess(): void {
238
238
  this.consecutiveFailures = 0;
239
- this.circuitState = 'closed';
239
+ this.circuitState = "closed";
240
240
  }
241
241
 
242
242
  /**
@@ -245,15 +245,17 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
245
245
  private onFailure(): void {
246
246
  this.consecutiveFailures++;
247
247
 
248
- if (this.circuitState === 'half-open') {
248
+ if (this.circuitState === "half-open") {
249
249
  // Failed during half-open - re-open the circuit
250
- this.circuitState = 'open';
250
+ this.circuitState = "open";
251
251
  this.circuitOpenedAt = Date.now();
252
252
  } else if (this.consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
253
253
  // Threshold reached - open the circuit
254
- this.circuitState = 'open';
254
+ this.circuitState = "open";
255
255
  this.circuitOpenedAt = Date.now();
256
- console.warn(`[GatewayClient] Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`);
256
+ console.warn(
257
+ `[GatewayClient] Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`
258
+ );
257
259
  }
258
260
  }
259
261
 
@@ -264,7 +266,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
264
266
  return {
265
267
  state: this.circuitState,
266
268
  failures: this.consecutiveFailures,
267
- openedAt: this.circuitState === 'open' ? this.circuitOpenedAt : null,
269
+ openedAt: this.circuitState === "open" ? this.circuitOpenedAt : null,
268
270
  };
269
271
  }
270
272
 
@@ -272,7 +274,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
272
274
  * F3: Manually reset the circuit breaker
273
275
  */
274
276
  resetCircuitBreaker(): void {
275
- this.circuitState = 'closed';
277
+ this.circuitState = "closed";
276
278
  this.consecutiveFailures = 0;
277
279
  this.circuitOpenedAt = 0;
278
280
  }
@@ -295,25 +297,25 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
295
297
 
296
298
  // Validate required params
297
299
  if (!gatewayUrl || !contentId) {
298
- const error = 'Missing required parameters: gatewayUrl or contentId';
299
- this.setStatus('error', error);
300
+ const error = "Missing required parameters: gatewayUrl or contentId";
301
+ this.setStatus("error", error);
300
302
  throw new Error(error);
301
303
  }
302
304
 
303
- this.setStatus('loading');
305
+ this.setStatus("loading");
304
306
 
305
307
  const ac = new AbortController();
306
308
  this.abortController = ac;
307
309
 
308
310
  try {
309
- const graphqlEndpoint = gatewayUrl.replace(/\/$/, '');
311
+ const graphqlEndpoint = gatewayUrl.replace(/\/$/, "");
310
312
 
311
313
  const res = await fetchWithRetry(
312
314
  graphqlEndpoint,
313
315
  {
314
- method: 'POST',
316
+ method: "POST",
315
317
  headers: {
316
- 'Content-Type': 'application/json',
318
+ "Content-Type": "application/json",
317
319
  ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
318
320
  },
319
321
  body: JSON.stringify({
@@ -333,7 +335,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
333
335
  const payload = await res.json();
334
336
 
335
337
  if (payload.errors?.length) {
336
- throw new Error(payload.errors[0]?.message || 'GraphQL error');
338
+ throw new Error(payload.errors[0]?.message || "GraphQL error");
337
339
  }
338
340
 
339
341
  const resp = payload.data?.resolveViewerEndpoint;
@@ -341,7 +343,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
341
343
  const fallbacks = Array.isArray(resp?.fallbacks) ? resp.fallbacks : [];
342
344
 
343
345
  if (!primary) {
344
- throw new Error('No endpoints available');
346
+ throw new Error("No endpoints available");
345
347
  }
346
348
 
347
349
  const endpoints: ContentEndpoints = {
@@ -353,19 +355,19 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
353
355
  this.endpoints = endpoints;
354
356
  // F2: Update cache timestamp
355
357
  this.cacheTimestamp = Date.now();
356
- this.setStatus('ready');
357
- this.emit('endpointsResolved', { endpoints });
358
+ this.setStatus("ready");
359
+ this.emit("endpointsResolved", { endpoints });
358
360
 
359
361
  return endpoints;
360
362
  } catch (e) {
361
363
  // Ignore abort errors
362
364
  if (ac.signal.aborted) {
363
- throw new Error('Request aborted');
365
+ throw new Error("Request aborted");
364
366
  }
365
367
 
366
- const message = e instanceof Error ? e.message : 'Unknown gateway error';
367
- console.error('[GatewayClient] Gateway resolution failed:', message);
368
- this.setStatus('error', message);
368
+ const message = e instanceof Error ? e.message : "Unknown gateway error";
369
+ console.error("[GatewayClient] Gateway resolution failed:", message);
370
+ this.setStatus("error", message);
369
371
  throw new Error(message);
370
372
  }
371
373
  }
@@ -416,7 +418,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
416
418
  this.inFlightRequest = null;
417
419
  // F3: Reset circuit breaker for new config
418
420
  this.resetCircuitBreaker();
419
- this.setStatus('idle');
421
+ this.setStatus("idle");
420
422
  }
421
423
 
422
424
  /**
@@ -430,7 +432,7 @@ export class GatewayClient extends TypedEventEmitter<GatewayClientEvents> {
430
432
  private setStatus(status: GatewayStatus, error?: string): void {
431
433
  this.status = status;
432
434
  this.error = error ?? null;
433
- this.emit('statusChange', { status, error });
435
+ this.emit("statusChange", { status, error });
434
436
  }
435
437
  }
436
438