@livepeer-frameworks/player-core 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/cjs/index.js +19493 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +19398 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/player.css +2140 -0
  6. package/dist/types/core/ABRController.d.ts +164 -0
  7. package/dist/types/core/CodecUtils.d.ts +54 -0
  8. package/dist/types/core/Disposable.d.ts +61 -0
  9. package/dist/types/core/EventEmitter.d.ts +73 -0
  10. package/dist/types/core/GatewayClient.d.ts +144 -0
  11. package/dist/types/core/InteractionController.d.ts +121 -0
  12. package/dist/types/core/LiveDurationProxy.d.ts +102 -0
  13. package/dist/types/core/MetaTrackManager.d.ts +220 -0
  14. package/dist/types/core/MistReporter.d.ts +163 -0
  15. package/dist/types/core/MistSignaling.d.ts +148 -0
  16. package/dist/types/core/PlayerController.d.ts +665 -0
  17. package/dist/types/core/PlayerInterface.d.ts +230 -0
  18. package/dist/types/core/PlayerManager.d.ts +182 -0
  19. package/dist/types/core/PlayerRegistry.d.ts +27 -0
  20. package/dist/types/core/QualityMonitor.d.ts +184 -0
  21. package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
  22. package/dist/types/core/SeekingUtils.d.ts +142 -0
  23. package/dist/types/core/StreamStateClient.d.ts +108 -0
  24. package/dist/types/core/SubtitleManager.d.ts +111 -0
  25. package/dist/types/core/TelemetryReporter.d.ts +79 -0
  26. package/dist/types/core/TimeFormat.d.ts +97 -0
  27. package/dist/types/core/TimerManager.d.ts +83 -0
  28. package/dist/types/core/UrlUtils.d.ts +81 -0
  29. package/dist/types/core/detector.d.ts +149 -0
  30. package/dist/types/core/index.d.ts +49 -0
  31. package/dist/types/core/scorer.d.ts +167 -0
  32. package/dist/types/core/selector.d.ts +9 -0
  33. package/dist/types/index.d.ts +45 -0
  34. package/dist/types/lib/utils.d.ts +2 -0
  35. package/dist/types/players/DashJsPlayer.d.ts +102 -0
  36. package/dist/types/players/HlsJsPlayer.d.ts +70 -0
  37. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
  38. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
  39. package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
  40. package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
  41. package/dist/types/players/MistPlayer.d.ts +25 -0
  42. package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
  43. package/dist/types/players/NativePlayer.d.ts +143 -0
  44. package/dist/types/players/VideoJsPlayer.d.ts +59 -0
  45. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
  46. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
  47. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
  48. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
  49. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
  50. package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
  51. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
  52. package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
  53. package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
  54. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
  55. package/dist/types/players/index.d.ts +14 -0
  56. package/dist/types/styles/index.d.ts +11 -0
  57. package/dist/types/types.d.ts +363 -0
  58. package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
  59. package/dist/types/vanilla/index.d.ts +19 -0
  60. package/dist/workers/decoder.worker.js +989 -0
  61. package/dist/workers/decoder.worker.js.map +1 -0
  62. package/package.json +80 -0
  63. package/src/core/ABRController.ts +550 -0
  64. package/src/core/CodecUtils.ts +257 -0
  65. package/src/core/Disposable.ts +120 -0
  66. package/src/core/EventEmitter.ts +113 -0
  67. package/src/core/GatewayClient.ts +439 -0
  68. package/src/core/InteractionController.ts +712 -0
  69. package/src/core/LiveDurationProxy.ts +270 -0
  70. package/src/core/MetaTrackManager.ts +753 -0
  71. package/src/core/MistReporter.ts +543 -0
  72. package/src/core/MistSignaling.ts +346 -0
  73. package/src/core/PlayerController.ts +2829 -0
  74. package/src/core/PlayerInterface.ts +432 -0
  75. package/src/core/PlayerManager.ts +900 -0
  76. package/src/core/PlayerRegistry.ts +149 -0
  77. package/src/core/QualityMonitor.ts +597 -0
  78. package/src/core/ScreenWakeLockManager.ts +163 -0
  79. package/src/core/SeekingUtils.ts +364 -0
  80. package/src/core/StreamStateClient.ts +457 -0
  81. package/src/core/SubtitleManager.ts +297 -0
  82. package/src/core/TelemetryReporter.ts +308 -0
  83. package/src/core/TimeFormat.ts +205 -0
  84. package/src/core/TimerManager.ts +209 -0
  85. package/src/core/UrlUtils.ts +179 -0
  86. package/src/core/detector.ts +382 -0
  87. package/src/core/index.ts +140 -0
  88. package/src/core/scorer.ts +553 -0
  89. package/src/core/selector.ts +16 -0
  90. package/src/global.d.ts +11 -0
  91. package/src/index.ts +75 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/players/DashJsPlayer.ts +642 -0
  94. package/src/players/HlsJsPlayer.ts +483 -0
  95. package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
  96. package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
  97. package/src/players/MewsWsPlayer/index.ts +1065 -0
  98. package/src/players/MewsWsPlayer/types.ts +106 -0
  99. package/src/players/MistPlayer.ts +188 -0
  100. package/src/players/MistWebRTCPlayer/index.ts +703 -0
  101. package/src/players/NativePlayer.ts +820 -0
  102. package/src/players/VideoJsPlayer.ts +643 -0
  103. package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
  104. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
  105. package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
  106. package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
  107. package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
  108. package/src/players/WebCodecsPlayer/index.ts +1650 -0
  109. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
  110. package/src/players/WebCodecsPlayer/types.ts +542 -0
  111. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
  112. package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
  113. package/src/players/index.ts +22 -0
  114. package/src/styles/animations.css +21 -0
  115. package/src/styles/index.ts +52 -0
  116. package/src/styles/player.css +2126 -0
  117. package/src/styles/tailwind.css +1015 -0
  118. package/src/types.ts +421 -0
  119. package/src/vanilla/FrameWorksPlayer.ts +367 -0
  120. package/src/vanilla/index.ts +22 -0
@@ -0,0 +1,543 @@
1
+ /**
2
+ * MistReporter - Reports playback stats to MistServer
3
+ *
4
+ * Implements the same reporting protocol as MistPlayer reference:
5
+ * - Sends initial report on player selection (player, sourceType, sourceUrl, pageUrl)
6
+ * - Reports deltas every 5 seconds
7
+ * - Tracks waiting/stalled events and durations
8
+ * - Integrates with QualityMonitor for playbackScore
9
+ * - Sends final report on unload
10
+ *
11
+ * Reports are sent over the same WebSocket used for stream state.
12
+ */
13
+
14
+ import { TimerManager } from './TimerManager';
15
+
16
+ export interface MistReporterStats {
17
+ nWaiting: number;
18
+ timeWaiting: number;
19
+ nStalled: number;
20
+ timeStalled: number;
21
+ timeUnpaused: number;
22
+ nError: number;
23
+ lastError: string | null;
24
+ firstPlayback: number | null;
25
+ playbackScore: number;
26
+ autoplay: 'success' | 'muted' | 'failed' | null;
27
+ videoHeight: number | null;
28
+ videoWidth: number | null;
29
+ playerHeight: number | null;
30
+ playerWidth: number | null;
31
+ tracks: string | null;
32
+ nLog: number;
33
+ }
34
+
35
+ export interface MistReporterInitialReport {
36
+ player: string;
37
+ sourceType: string;
38
+ sourceUrl: string;
39
+ pageUrl: string;
40
+ }
41
+
42
+ export interface MistReporterOptions {
43
+ /** WebSocket to send reports through (shared with stream state) */
44
+ socket?: WebSocket | null;
45
+ /** Report interval in ms (default: 5000) */
46
+ reportInterval?: number;
47
+ /** E2: Batch flush interval in ms (default: 1000) - max rate for non-critical reports */
48
+ batchFlushInterval?: number;
49
+ /** Boot timestamp for firstPlayback calculation */
50
+ bootMs?: number;
51
+ /** Log array reference for including logs in reports */
52
+ logs?: string[];
53
+ }
54
+
55
+ type StatsKey = keyof MistReporterStats;
56
+
57
+ /**
58
+ * MistReporter - Playback telemetry to MistServer
59
+ */
60
+ export class MistReporter {
61
+ private socket: WebSocket | null = null;
62
+ private videoElement: HTMLVideoElement | null = null;
63
+ private containerElement: HTMLElement | null = null;
64
+ private reportInterval: number;
65
+ private batchFlushInterval: number;
66
+ private bootMs: number;
67
+ private logs: string[] = [];
68
+
69
+ // Internal stats storage (different structure for time tracking)
70
+ private _stats: {
71
+ _nWaiting: number;
72
+ _nStalled: number;
73
+ _nError: number;
74
+ lastError: string | null;
75
+ firstPlayback: number | null;
76
+ playbackScore: number;
77
+ autoplay: 'success' | 'muted' | 'failed' | null;
78
+ tracks: string | null;
79
+ };
80
+
81
+ // Time tracking state
82
+ private waitingSince: number = 0;
83
+ private stalledSince: number = 0;
84
+ private unpausedSince: number = 0;
85
+ private timeWaitingAccum: number = 0;
86
+ private timeStalledAccum: number = 0;
87
+ private timeUnpausedAccum: number = 0;
88
+
89
+ // Last reported values for delta detection
90
+ private lastReported: Partial<MistReporterStats> = {};
91
+
92
+ // Timer manager for periodic reporting
93
+ private timers = new TimerManager();
94
+
95
+ // Event listener cleanup functions
96
+ private listeners: Array<() => void> = [];
97
+
98
+ // Track if first playback has been recorded
99
+ private firstPlaybackRecorded = false;
100
+
101
+ // E2: Batch reporting state
102
+ private pendingBatch: Record<string, unknown> = {};
103
+ private hasPendingBatch = false;
104
+ private lastFlushTime = 0;
105
+ private batchFlushTimerId: number | null = null;
106
+
107
+ // E3: Offline queue for telemetry
108
+ private offlineQueue: Record<string, unknown>[] = [];
109
+ private static readonly MAX_OFFLINE_QUEUE_SIZE = 100;
110
+
111
+ constructor(options: MistReporterOptions = {}) {
112
+ this.socket = options.socket ?? null;
113
+ this.reportInterval = options.reportInterval ?? 5000;
114
+ this.batchFlushInterval = options.batchFlushInterval ?? 1000;
115
+ this.bootMs = options.bootMs ?? Date.now();
116
+ this.logs = options.logs ?? [];
117
+
118
+ // Initialize stats
119
+ this._stats = {
120
+ _nWaiting: 0,
121
+ _nStalled: 0,
122
+ _nError: 0,
123
+ lastError: null,
124
+ firstPlayback: null,
125
+ playbackScore: 1.0,
126
+ autoplay: null,
127
+ tracks: null,
128
+ };
129
+
130
+ // Initialize lastReported to empty
131
+ this.lastReported = {
132
+ nWaiting: 0,
133
+ timeWaiting: 0,
134
+ nStalled: 0,
135
+ timeStalled: 0,
136
+ timeUnpaused: 0,
137
+ nError: 0,
138
+ lastError: null,
139
+ firstPlayback: null,
140
+ playbackScore: 1,
141
+ autoplay: null,
142
+ videoHeight: null,
143
+ videoWidth: null,
144
+ playerHeight: null,
145
+ playerWidth: null,
146
+ tracks: null,
147
+ nLog: 0,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Set the WebSocket to use for reporting
153
+ * E3: Flushes offline queue when socket becomes available
154
+ */
155
+ setSocket(socket: WebSocket | null): void {
156
+ const wasDisconnected = !this.socket || this.socket.readyState !== WebSocket.OPEN;
157
+ this.socket = socket;
158
+
159
+ // E3: Flush offline queue when reconnecting
160
+ if (wasDisconnected && socket?.readyState === WebSocket.OPEN) {
161
+ this.flushOfflineQueue();
162
+ }
163
+ }
164
+
165
+ /**
166
+ * E3: Flush queued reports that were collected while offline
167
+ */
168
+ private flushOfflineQueue(): void {
169
+ if (this.offlineQueue.length === 0) {
170
+ return;
171
+ }
172
+
173
+ // Send all queued reports
174
+ for (const report of this.offlineQueue) {
175
+ this.sendReport(report);
176
+ }
177
+
178
+ this.offlineQueue = [];
179
+ }
180
+
181
+ /**
182
+ * Get current stats object with computed getters
183
+ * E1: Uses performance.now() for sub-millisecond precision in duration tracking
184
+ */
185
+ getStats(): MistReporterStats {
186
+ const now = performance.now();
187
+
188
+ return {
189
+ nWaiting: this._stats._nWaiting,
190
+ timeWaiting: Math.round(this.timeWaitingAccum + (this.waitingSince > 0 ? now - this.waitingSince : 0)),
191
+ nStalled: this._stats._nStalled,
192
+ timeStalled: Math.round(this.timeStalledAccum + (this.stalledSince > 0 ? now - this.stalledSince : 0)),
193
+ timeUnpaused: Math.round(this.timeUnpausedAccum + (this.unpausedSince > 0 ? now - this.unpausedSince : 0)),
194
+ nError: this._stats._nError,
195
+ lastError: this._stats.lastError,
196
+ firstPlayback: this._stats.firstPlayback,
197
+ playbackScore: Math.round(this._stats.playbackScore * 10) / 10,
198
+ autoplay: this._stats.autoplay,
199
+ videoHeight: this.videoElement?.videoHeight ?? null,
200
+ videoWidth: this.videoElement?.videoWidth ?? null,
201
+ playerHeight: this.containerElement?.clientHeight ?? null,
202
+ playerWidth: this.containerElement?.clientWidth ?? null,
203
+ tracks: this._stats.tracks,
204
+ nLog: this.logs.length,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Set a stat value
210
+ */
211
+ set<K extends StatsKey>(key: K, value: MistReporterStats[K]): void {
212
+ if (key === 'nWaiting') {
213
+ this._stats._nWaiting = value as number;
214
+ } else if (key === 'nStalled') {
215
+ this._stats._nStalled = value as number;
216
+ } else if (key === 'nError') {
217
+ this._stats._nError = value as number;
218
+ } else if (key === 'lastError') {
219
+ this._stats.lastError = value as string | null;
220
+ } else if (key === 'firstPlayback') {
221
+ this._stats.firstPlayback = value as number | null;
222
+ } else if (key === 'playbackScore') {
223
+ this._stats.playbackScore = value as number;
224
+ } else if (key === 'autoplay') {
225
+ this._stats.autoplay = value as 'success' | 'muted' | 'failed' | null;
226
+ } else if (key === 'tracks') {
227
+ this._stats.tracks = value as string | null;
228
+ }
229
+ // Other keys (computed) are read-only
230
+ }
231
+
232
+ /**
233
+ * Increment a counter stat
234
+ */
235
+ add(key: 'nWaiting' | 'nStalled' | 'nError', amount = 1): void {
236
+ if (key === 'nWaiting') {
237
+ this._stats._nWaiting += amount;
238
+ } else if (key === 'nStalled') {
239
+ this._stats._nStalled += amount;
240
+ } else if (key === 'nError') {
241
+ this._stats._nError += amount;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Initialize reporting for a video element
247
+ */
248
+ init(videoElement: HTMLVideoElement, containerElement?: HTMLElement): void {
249
+ this.videoElement = videoElement;
250
+ this.containerElement = containerElement ?? videoElement.parentElement ?? null;
251
+ this.firstPlaybackRecorded = false;
252
+
253
+ // Set up event listeners like MistPlayer reference
254
+ // E1: Use performance.now() for sub-millisecond precision in duration tracking
255
+ const onPlaying = () => {
256
+ const now = performance.now();
257
+ const isFirstPlay = !this.firstPlaybackRecorded;
258
+
259
+ // Record first playback time (still uses Date.now for absolute timestamp)
260
+ if (isFirstPlay) {
261
+ this._stats.firstPlayback = Date.now() - this.bootMs;
262
+ this.firstPlaybackRecorded = true;
263
+ }
264
+
265
+ // End waiting state if active
266
+ if (this.waitingSince > 0) {
267
+ this.timeWaitingAccum += now - this.waitingSince;
268
+ this.waitingSince = 0;
269
+ }
270
+
271
+ // End stalled state if active
272
+ if (this.stalledSince > 0) {
273
+ this.timeStalledAccum += now - this.stalledSince;
274
+ this.stalledSince = 0;
275
+ }
276
+
277
+ // Start unpaused timer
278
+ if (this.unpausedSince === 0) {
279
+ this.unpausedSince = now;
280
+ }
281
+
282
+ // E2: Flush immediately on first playback
283
+ if (isFirstPlay) {
284
+ this.reportStats();
285
+ this.flushBatch();
286
+ }
287
+ };
288
+
289
+ const onWaiting = () => {
290
+ this._stats._nWaiting++;
291
+ if (this.waitingSince === 0) {
292
+ this.waitingSince = performance.now();
293
+ }
294
+ };
295
+
296
+ const onStalled = () => {
297
+ this._stats._nStalled++;
298
+ if (this.stalledSince === 0) {
299
+ this.stalledSince = performance.now();
300
+ }
301
+ };
302
+
303
+ const onPause = () => {
304
+ // End unpaused timer
305
+ if (this.unpausedSince > 0) {
306
+ this.timeUnpausedAccum += performance.now() - this.unpausedSince;
307
+ this.unpausedSince = 0;
308
+ }
309
+ };
310
+
311
+ const onError = (e: Event) => {
312
+ this._stats._nError++;
313
+ const error = (e as ErrorEvent).message || 'Unknown error';
314
+ this._stats.lastError = error;
315
+
316
+ // E2: Flush immediately on error
317
+ this.reportStats();
318
+ this.flushBatch();
319
+ };
320
+
321
+ const onCanPlay = () => {
322
+ // End waiting state if active
323
+ if (this.waitingSince > 0) {
324
+ this.timeWaitingAccum += performance.now() - this.waitingSince;
325
+ this.waitingSince = 0;
326
+ }
327
+ };
328
+
329
+ videoElement.addEventListener('playing', onPlaying);
330
+ videoElement.addEventListener('waiting', onWaiting);
331
+ videoElement.addEventListener('stalled', onStalled);
332
+ videoElement.addEventListener('pause', onPause);
333
+ videoElement.addEventListener('error', onError);
334
+ videoElement.addEventListener('canplay', onCanPlay);
335
+
336
+ this.listeners = [
337
+ () => videoElement.removeEventListener('playing', onPlaying),
338
+ () => videoElement.removeEventListener('waiting', onWaiting),
339
+ () => videoElement.removeEventListener('stalled', onStalled),
340
+ () => videoElement.removeEventListener('pause', onPause),
341
+ () => videoElement.removeEventListener('error', onError),
342
+ () => videoElement.removeEventListener('canplay', onCanPlay),
343
+ ];
344
+
345
+ // Start periodic reporting
346
+ this.startReporting();
347
+ }
348
+
349
+ /**
350
+ * Send initial report when player is selected
351
+ */
352
+ sendInitialReport(info: MistReporterInitialReport): void {
353
+ this.report(info as unknown as Record<string, unknown>);
354
+ }
355
+
356
+ /**
357
+ * Update playback score (call from QualityMonitor)
358
+ */
359
+ setPlaybackScore(score: number): void {
360
+ this._stats.playbackScore = score;
361
+ }
362
+
363
+ /**
364
+ * Update autoplay status
365
+ */
366
+ setAutoplayStatus(status: 'success' | 'muted' | 'failed'): void {
367
+ this._stats.autoplay = status;
368
+ }
369
+
370
+ /**
371
+ * Update current tracks
372
+ */
373
+ setTracks(tracks: string[]): void {
374
+ this._stats.tracks = tracks.join(',');
375
+ }
376
+
377
+ /**
378
+ * Send a report over WebSocket immediately
379
+ * E3: Queues reports when socket is unavailable (up to MAX_OFFLINE_QUEUE_SIZE)
380
+ */
381
+ private sendReport(data: Record<string, unknown>): void {
382
+ // If socket not available, queue for later
383
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
384
+ // E3: Queue report for when socket reconnects
385
+ if (this.offlineQueue.length < MistReporter.MAX_OFFLINE_QUEUE_SIZE) {
386
+ this.offlineQueue.push(data);
387
+ }
388
+ // If queue is full, oldest reports are lost (FIFO overflow)
389
+ return;
390
+ }
391
+
392
+ try {
393
+ this.socket.send(JSON.stringify(data));
394
+ } catch {
395
+ // E3: Queue on send failure too
396
+ if (this.offlineQueue.length < MistReporter.MAX_OFFLINE_QUEUE_SIZE) {
397
+ this.offlineQueue.push(data);
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * E2: Queue data for batched reporting
404
+ * Merges with pending batch, schedules flush if not already pending
405
+ */
406
+ private report(data: Record<string, unknown>): void {
407
+ // Merge into pending batch
408
+ Object.assign(this.pendingBatch, data);
409
+ this.hasPendingBatch = true;
410
+
411
+ // Schedule flush if not already scheduled
412
+ if (this.batchFlushTimerId === null) {
413
+ const now = performance.now();
414
+ const timeSinceLastFlush = now - this.lastFlushTime;
415
+ const delay = Math.max(0, this.batchFlushInterval - timeSinceLastFlush);
416
+
417
+ this.batchFlushTimerId = this.timers.start(() => {
418
+ this.batchFlushTimerId = null;
419
+ this.flushBatch();
420
+ }, delay, 'batchFlush');
421
+ }
422
+ }
423
+
424
+ /**
425
+ * E2: Flush pending batch immediately
426
+ * Used for critical events (error, first play, unload)
427
+ */
428
+ flushBatch(): void {
429
+ if (this.batchFlushTimerId !== null) {
430
+ this.timers.stop(this.batchFlushTimerId);
431
+ this.batchFlushTimerId = null;
432
+ }
433
+
434
+ if (!this.hasPendingBatch) {
435
+ return;
436
+ }
437
+
438
+ this.sendReport(this.pendingBatch);
439
+ this.pendingBatch = {};
440
+ this.hasPendingBatch = false;
441
+ this.lastFlushTime = performance.now();
442
+ }
443
+
444
+ /**
445
+ * Report stats delta (only changed values)
446
+ * E2: Now queues to batch instead of sending immediately
447
+ */
448
+ reportStats(): void {
449
+ const current = this.getStats();
450
+ const delta: Record<string, unknown> = {};
451
+ let hasChanges = false;
452
+
453
+ // Compare each stat and include only changed values
454
+ const keys: StatsKey[] = [
455
+ 'nWaiting', 'timeWaiting', 'nStalled', 'timeStalled', 'timeUnpaused',
456
+ 'nError', 'lastError', 'firstPlayback', 'playbackScore', 'autoplay',
457
+ 'videoHeight', 'videoWidth', 'playerHeight', 'playerWidth', 'tracks', 'nLog'
458
+ ];
459
+
460
+ for (const key of keys) {
461
+ const currentValue = current[key];
462
+ const lastValue = this.lastReported[key];
463
+
464
+ if (currentValue !== lastValue) {
465
+ delta[key] = currentValue;
466
+ (this.lastReported as Record<string, unknown>)[key] = currentValue;
467
+ hasChanges = true;
468
+ }
469
+ }
470
+
471
+ // Include logs if there are new ones
472
+ const lastLogCount = this.lastReported.nLog ?? 0;
473
+ if (this.logs.length > lastLogCount) {
474
+ const newLogs = this.logs.slice(lastLogCount);
475
+ if (newLogs.length > 0) {
476
+ delta.logs = newLogs;
477
+ hasChanges = true;
478
+ }
479
+ }
480
+
481
+ if (hasChanges) {
482
+ this.report(delta);
483
+ }
484
+
485
+ // Schedule next report
486
+ this.scheduleNextReport();
487
+ }
488
+
489
+ /**
490
+ * Start periodic reporting
491
+ */
492
+ private startReporting(): void {
493
+ this.scheduleNextReport();
494
+ }
495
+
496
+ /**
497
+ * Schedule the next report
498
+ */
499
+ private scheduleNextReport(): void {
500
+ this.timers.start(() => {
501
+ this.reportStats();
502
+ }, this.reportInterval, 'report');
503
+ }
504
+
505
+ /**
506
+ * Send final report and cleanup
507
+ * E2: Flushes immediately since this is a critical event
508
+ */
509
+ sendFinalReport(reason?: string): void {
510
+ // Report final stats
511
+ this.reportStats();
512
+
513
+ // Queue unload report
514
+ this.report({ unload: reason ?? null });
515
+
516
+ // E2: Flush immediately - don't wait for batch interval
517
+ this.flushBatch();
518
+ }
519
+
520
+ /**
521
+ * Stop reporting and cleanup
522
+ */
523
+ destroy(): void {
524
+ // Stop all timers
525
+ this.timers.destroy();
526
+
527
+ // Remove event listeners
528
+ this.listeners.forEach(cleanup => cleanup());
529
+ this.listeners = [];
530
+
531
+ this.videoElement = null;
532
+ this.containerElement = null;
533
+ }
534
+
535
+ /**
536
+ * Add a log entry
537
+ */
538
+ log(message: string): void {
539
+ this.logs.push(message);
540
+ }
541
+ }
542
+
543
+ export default MistReporter;