@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.
- package/dist/cjs/index.js +19493 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +19398 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/player.css +2140 -0
- package/dist/types/core/ABRController.d.ts +164 -0
- package/dist/types/core/CodecUtils.d.ts +54 -0
- package/dist/types/core/Disposable.d.ts +61 -0
- package/dist/types/core/EventEmitter.d.ts +73 -0
- package/dist/types/core/GatewayClient.d.ts +144 -0
- package/dist/types/core/InteractionController.d.ts +121 -0
- package/dist/types/core/LiveDurationProxy.d.ts +102 -0
- package/dist/types/core/MetaTrackManager.d.ts +220 -0
- package/dist/types/core/MistReporter.d.ts +163 -0
- package/dist/types/core/MistSignaling.d.ts +148 -0
- package/dist/types/core/PlayerController.d.ts +665 -0
- package/dist/types/core/PlayerInterface.d.ts +230 -0
- package/dist/types/core/PlayerManager.d.ts +182 -0
- package/dist/types/core/PlayerRegistry.d.ts +27 -0
- package/dist/types/core/QualityMonitor.d.ts +184 -0
- package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
- package/dist/types/core/SeekingUtils.d.ts +142 -0
- package/dist/types/core/StreamStateClient.d.ts +108 -0
- package/dist/types/core/SubtitleManager.d.ts +111 -0
- package/dist/types/core/TelemetryReporter.d.ts +79 -0
- package/dist/types/core/TimeFormat.d.ts +97 -0
- package/dist/types/core/TimerManager.d.ts +83 -0
- package/dist/types/core/UrlUtils.d.ts +81 -0
- package/dist/types/core/detector.d.ts +149 -0
- package/dist/types/core/index.d.ts +49 -0
- package/dist/types/core/scorer.d.ts +167 -0
- package/dist/types/core/selector.d.ts +9 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/players/DashJsPlayer.d.ts +102 -0
- package/dist/types/players/HlsJsPlayer.d.ts +70 -0
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
- package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
- package/dist/types/players/MistPlayer.d.ts +25 -0
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
- package/dist/types/players/NativePlayer.d.ts +143 -0
- package/dist/types/players/VideoJsPlayer.d.ts +59 -0
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
- package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
- package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
- package/dist/types/players/index.d.ts +14 -0
- package/dist/types/styles/index.d.ts +11 -0
- package/dist/types/types.d.ts +363 -0
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
- package/dist/types/vanilla/index.d.ts +19 -0
- package/dist/workers/decoder.worker.js +989 -0
- package/dist/workers/decoder.worker.js.map +1 -0
- package/package.json +80 -0
- package/src/core/ABRController.ts +550 -0
- package/src/core/CodecUtils.ts +257 -0
- package/src/core/Disposable.ts +120 -0
- package/src/core/EventEmitter.ts +113 -0
- package/src/core/GatewayClient.ts +439 -0
- package/src/core/InteractionController.ts +712 -0
- package/src/core/LiveDurationProxy.ts +270 -0
- package/src/core/MetaTrackManager.ts +753 -0
- package/src/core/MistReporter.ts +543 -0
- package/src/core/MistSignaling.ts +346 -0
- package/src/core/PlayerController.ts +2829 -0
- package/src/core/PlayerInterface.ts +432 -0
- package/src/core/PlayerManager.ts +900 -0
- package/src/core/PlayerRegistry.ts +149 -0
- package/src/core/QualityMonitor.ts +597 -0
- package/src/core/ScreenWakeLockManager.ts +163 -0
- package/src/core/SeekingUtils.ts +364 -0
- package/src/core/StreamStateClient.ts +457 -0
- package/src/core/SubtitleManager.ts +297 -0
- package/src/core/TelemetryReporter.ts +308 -0
- package/src/core/TimeFormat.ts +205 -0
- package/src/core/TimerManager.ts +209 -0
- package/src/core/UrlUtils.ts +179 -0
- package/src/core/detector.ts +382 -0
- package/src/core/index.ts +140 -0
- package/src/core/scorer.ts +553 -0
- package/src/core/selector.ts +16 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/players/DashJsPlayer.ts +642 -0
- package/src/players/HlsJsPlayer.ts +483 -0
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
- package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
- package/src/players/MewsWsPlayer/index.ts +1065 -0
- package/src/players/MewsWsPlayer/types.ts +106 -0
- package/src/players/MistPlayer.ts +188 -0
- package/src/players/MistWebRTCPlayer/index.ts +703 -0
- package/src/players/NativePlayer.ts +820 -0
- package/src/players/VideoJsPlayer.ts +643 -0
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
- package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
- package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
- package/src/players/WebCodecsPlayer/index.ts +1650 -0
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
- package/src/players/WebCodecsPlayer/types.ts +542 -0
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
- package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
- package/src/players/index.ts +22 -0
- package/src/styles/animations.css +21 -0
- package/src/styles/index.ts +52 -0
- package/src/styles/player.css +2126 -0
- package/src/styles/tailwind.css +1015 -0
- package/src/types.ts +421 -0
- package/src/vanilla/FrameWorksPlayer.ts +367 -0
- 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;
|