@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,297 @@
1
+ /**
2
+ * SubtitleManager - WebVTT subtitle track management
3
+ *
4
+ * Based on MistMetaPlayer's subtitle handling (wrappers/html5.js, webrtc.js).
5
+ * Manages text tracks on video elements with support for:
6
+ * - Loading WebVTT from MistServer URLs
7
+ * - Multiple subtitle track selection
8
+ * - Sync correction for WebRTC seek offsets
9
+ */
10
+
11
+ export interface SubtitleTrackInfo {
12
+ /** Track ID (from MistServer) */
13
+ id: string;
14
+ /** Display label */
15
+ label: string;
16
+ /** Language code (e.g., 'en', 'es') */
17
+ lang: string;
18
+ /** Source URL for WebVTT file */
19
+ src: string;
20
+ /** Whether this is the default track */
21
+ default?: boolean;
22
+ }
23
+
24
+ export interface SubtitleManagerConfig {
25
+ /** Base URL for MistServer (for constructing track URLs) */
26
+ mistBaseUrl?: string;
27
+ /** Stream name */
28
+ streamName?: string;
29
+ /** URL append string (auth tokens, etc.) */
30
+ urlAppend?: string;
31
+ /** Debug logging */
32
+ debug?: boolean;
33
+ }
34
+
35
+ /**
36
+ * SubtitleManager handles text track lifecycle on a video element
37
+ */
38
+ export class SubtitleManager {
39
+ private video: HTMLVideoElement | null = null;
40
+ private config: SubtitleManagerConfig;
41
+ private currentTrackId: string | null = null;
42
+ private seekOffset = 0;
43
+ private debug: boolean;
44
+ private listeners: Array<() => void> = [];
45
+
46
+ constructor(config: SubtitleManagerConfig = {}) {
47
+ this.config = config;
48
+ this.debug = config.debug ?? false;
49
+ }
50
+
51
+ /**
52
+ * Attach to a video element
53
+ */
54
+ attach(video: HTMLVideoElement): void {
55
+ this.detach();
56
+ this.video = video;
57
+
58
+ // Listen for events that may require sync correction
59
+ const onLoadedData = () => this.correctSubtitleSync();
60
+ const onSeeked = () => this.correctSubtitleSync();
61
+
62
+ video.addEventListener('loadeddata', onLoadedData);
63
+ video.addEventListener('seeked', onSeeked);
64
+
65
+ this.listeners = [
66
+ () => video.removeEventListener('loadeddata', onLoadedData),
67
+ () => video.removeEventListener('seeked', onSeeked),
68
+ ];
69
+ }
70
+
71
+ /**
72
+ * Detach from video element
73
+ */
74
+ detach(): void {
75
+ this.listeners.forEach(fn => fn());
76
+ this.listeners = [];
77
+ this.removeAllTracks();
78
+ this.video = null;
79
+ this.currentTrackId = null;
80
+ }
81
+
82
+ /**
83
+ * Get available text tracks from the video element
84
+ */
85
+ getTextTracks(): TextTrack[] {
86
+ if (!this.video) return [];
87
+ return Array.from(this.video.textTracks);
88
+ }
89
+
90
+ /**
91
+ * Get all track elements from the video
92
+ */
93
+ getTrackElements(): HTMLTrackElement[] {
94
+ if (!this.video) return [];
95
+ return Array.from(this.video.querySelectorAll('track'));
96
+ }
97
+
98
+ /**
99
+ * Set the active subtitle track
100
+ * Pass null to disable subtitles
101
+ */
102
+ setSubtitle(track: SubtitleTrackInfo | null): void {
103
+ if (!this.video) {
104
+ this.log('Cannot set subtitle: no video element attached');
105
+ return;
106
+ }
107
+
108
+ // Remove existing subtitle tracks
109
+ this.removeAllTracks();
110
+
111
+ if (!track) {
112
+ this.currentTrackId = null;
113
+ this.log('Subtitles disabled');
114
+ return;
115
+ }
116
+
117
+ // Create new track element
118
+ const trackElement = document.createElement('track');
119
+ trackElement.kind = 'subtitles';
120
+ trackElement.label = track.label;
121
+ trackElement.srclang = track.lang;
122
+ trackElement.src = this.buildTrackUrl(track.src);
123
+ trackElement.default = true;
124
+
125
+ // Set up load handler for sync correction
126
+ trackElement.addEventListener('load', () => {
127
+ this.correctSubtitleSync();
128
+ });
129
+
130
+ this.video.appendChild(trackElement);
131
+ this.currentTrackId = track.id;
132
+
133
+ // Enable the track
134
+ const textTrack = this.video.textTracks[this.video.textTracks.length - 1];
135
+ if (textTrack) {
136
+ textTrack.mode = 'showing';
137
+ }
138
+
139
+ this.log(`Subtitle track set: ${track.label} (${track.lang})`);
140
+ }
141
+
142
+ /**
143
+ * Build track URL with base URL and append params
144
+ */
145
+ private buildTrackUrl(src: string): string {
146
+ let url = src;
147
+
148
+ // If relative URL and base URL provided, construct full URL
149
+ if (!url.startsWith('http') && this.config.mistBaseUrl) {
150
+ const base = this.config.mistBaseUrl.replace(/\/$/, '');
151
+ url = url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;
152
+ }
153
+
154
+ // Append URL params if configured
155
+ if (this.config.urlAppend) {
156
+ const separator = url.includes('?') ? '&' : '?';
157
+ url = `${url}${separator}${this.config.urlAppend}`;
158
+ }
159
+
160
+ return url;
161
+ }
162
+
163
+ /**
164
+ * Create subtitle track info from MistServer track metadata
165
+ */
166
+ static createTrackInfo(
167
+ trackId: string,
168
+ label: string,
169
+ lang: string,
170
+ baseUrl: string,
171
+ streamName: string
172
+ ): SubtitleTrackInfo {
173
+ // MistServer WebVTT URL format
174
+ const src = `${baseUrl}/${streamName}.vtt?track=${trackId}`;
175
+ return { id: trackId, label, lang, src };
176
+ }
177
+
178
+ /**
179
+ * Remove all track elements from video
180
+ */
181
+ removeAllTracks(): void {
182
+ if (!this.video) return;
183
+
184
+ const tracks = this.video.querySelectorAll('track');
185
+ tracks.forEach(track => track.remove());
186
+ }
187
+
188
+ /**
189
+ * Get currently active track ID
190
+ */
191
+ getCurrentTrackId(): string | null {
192
+ return this.currentTrackId;
193
+ }
194
+
195
+ /**
196
+ * Set seek offset for WebRTC sync correction
197
+ * WebRTC playback has a seek offset that needs to be applied to subtitle timing
198
+ */
199
+ setSeekOffset(offset: number): void {
200
+ const oldOffset = this.seekOffset;
201
+ this.seekOffset = offset;
202
+
203
+ // Re-sync if offset changed significantly
204
+ if (Math.abs(oldOffset - offset) > 1) {
205
+ this.correctSubtitleSync();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Correct subtitle timing based on seek offset
211
+ * This is needed for WebRTC where video.currentTime doesn't match actual playback position
212
+ */
213
+ private correctSubtitleSync(): void {
214
+ if (!this.video || this.video.textTracks.length === 0) return;
215
+
216
+ const textTrack = this.video.textTracks[0];
217
+ if (!textTrack || !textTrack.cues) return;
218
+
219
+ const currentOffset = (textTrack as any).currentOffset || 0;
220
+
221
+ // Don't bother if change is small
222
+ if (Math.abs(this.seekOffset - currentOffset) < 1) return;
223
+
224
+ this.log(`Correcting subtitle sync: offset ${currentOffset} -> ${this.seekOffset}`);
225
+
226
+ // Collect and re-add cues with corrected timing
227
+ const newCues: VTTCue[] = [];
228
+
229
+ for (let i = textTrack.cues.length - 1; i >= 0; i--) {
230
+ const cue = textTrack.cues[i] as VTTCue;
231
+ textTrack.removeCue(cue);
232
+
233
+ // Store original timing if not already stored
234
+ if (!(cue as any).orig) {
235
+ (cue as any).orig = { start: cue.startTime, end: cue.endTime };
236
+ }
237
+
238
+ // Apply offset correction
239
+ cue.startTime = (cue as any).orig.start - this.seekOffset;
240
+ cue.endTime = (cue as any).orig.end - this.seekOffset;
241
+
242
+ newCues.push(cue);
243
+ }
244
+
245
+ // Re-add cues
246
+ for (const cue of newCues) {
247
+ try {
248
+ textTrack.addCue(cue);
249
+ } catch (e) {
250
+ // Ignore errors from invalid cue timing
251
+ }
252
+ }
253
+
254
+ (textTrack as any).currentOffset = this.seekOffset;
255
+ }
256
+
257
+ /**
258
+ * Parse subtitle tracks from MistServer stream info
259
+ */
260
+ static parseTracksFromStreamInfo(
261
+ streamInfo: { meta?: { tracks?: Record<string, { type: string; codec: string; lang?: string }> } },
262
+ baseUrl: string,
263
+ streamName: string
264
+ ): SubtitleTrackInfo[] {
265
+ const tracks: SubtitleTrackInfo[] = [];
266
+
267
+ if (!streamInfo.meta?.tracks) return tracks;
268
+
269
+ for (const [trackId, trackData] of Object.entries(streamInfo.meta.tracks)) {
270
+ if (trackData.type === 'meta' && trackData.codec === 'subtitle') {
271
+ const lang = trackData.lang || 'und';
272
+ const label = lang === 'und' ? `Subtitles ${trackId}` : lang.toUpperCase();
273
+ tracks.push(SubtitleManager.createTrackInfo(trackId, label, lang, baseUrl, streamName));
274
+ }
275
+ }
276
+
277
+ return tracks;
278
+ }
279
+
280
+ /**
281
+ * Debug logging
282
+ */
283
+ private log(message: string): void {
284
+ if (this.debug) {
285
+ console.debug(`[SubtitleManager] ${message}`);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Cleanup
291
+ */
292
+ destroy(): void {
293
+ this.detach();
294
+ }
295
+ }
296
+
297
+ export default SubtitleManager;
@@ -0,0 +1,308 @@
1
+ import type { TelemetryPayload, TelemetryOptions, PlaybackQuality, ContentType } from '../types';
2
+
3
+ /**
4
+ * Generate a unique session ID
5
+ */
6
+ function generateSessionId(): string {
7
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`;
8
+ }
9
+
10
+ export interface TelemetryReporterConfig {
11
+ /** Telemetry endpoint URL */
12
+ endpoint: string;
13
+ /** Auth token for endpoint */
14
+ authToken?: string;
15
+ /** Report interval in ms (default: 5000) */
16
+ interval?: number;
17
+ /** Batch size before flush (default: 1) */
18
+ batchSize?: number;
19
+ /** Content ID being played */
20
+ contentId: string;
21
+ /** Content type */
22
+ contentType: ContentType;
23
+ /** Player type name */
24
+ playerType: string;
25
+ /** Protocol being used */
26
+ protocol: string;
27
+ }
28
+
29
+ /**
30
+ * TelemetryReporter - Sends playback metrics to server
31
+ *
32
+ * Features:
33
+ * - Batched reporting at configurable interval
34
+ * - Retry with exponential backoff on failure
35
+ * - Uses navigator.sendBeacon() for reliable page unload reporting
36
+ * - Tracks errors during playback
37
+ */
38
+ export class TelemetryReporter {
39
+ private config: Required<TelemetryReporterConfig>;
40
+ private sessionId: string;
41
+ private intervalId: ReturnType<typeof setInterval> | null = null;
42
+ private pendingPayloads: TelemetryPayload[] = [];
43
+ private errors: Array<{ code: string; message: string; timestamp: number }> = [];
44
+ private stallCount = 0;
45
+ private totalStallMs = 0;
46
+ private lastStallStart = 0;
47
+ private videoElement: HTMLVideoElement | null = null;
48
+ private qualityGetter: (() => PlaybackQuality | null) | null = null;
49
+ private listeners: Array<() => void> = [];
50
+
51
+ constructor(config: TelemetryReporterConfig) {
52
+ this.config = {
53
+ endpoint: config.endpoint,
54
+ authToken: config.authToken ?? '',
55
+ interval: config.interval ?? 5000,
56
+ batchSize: config.batchSize ?? 1,
57
+ contentId: config.contentId,
58
+ contentType: config.contentType,
59
+ playerType: config.playerType,
60
+ protocol: config.protocol,
61
+ };
62
+ this.sessionId = generateSessionId();
63
+ }
64
+
65
+ /**
66
+ * Start telemetry reporting
67
+ */
68
+ start(
69
+ videoElement: HTMLVideoElement,
70
+ qualityGetter?: () => PlaybackQuality | null
71
+ ): void {
72
+ this.stop();
73
+
74
+ this.videoElement = videoElement;
75
+ this.qualityGetter = qualityGetter ?? null;
76
+ this.stallCount = 0;
77
+ this.totalStallMs = 0;
78
+ this.errors = [];
79
+
80
+ // Track stalls
81
+ const onWaiting = () => {
82
+ this.stallCount++;
83
+ this.lastStallStart = performance.now();
84
+ };
85
+
86
+ const onPlaying = () => {
87
+ if (this.lastStallStart > 0) {
88
+ this.totalStallMs += performance.now() - this.lastStallStart;
89
+ this.lastStallStart = 0;
90
+ }
91
+ };
92
+
93
+ const onError = () => {
94
+ const error = videoElement.error;
95
+ if (error) {
96
+ this.errors.push({
97
+ code: String(error.code),
98
+ message: error.message || 'Unknown error',
99
+ timestamp: Date.now(),
100
+ });
101
+ }
102
+ };
103
+
104
+ videoElement.addEventListener('waiting', onWaiting);
105
+ videoElement.addEventListener('playing', onPlaying);
106
+ videoElement.addEventListener('error', onError);
107
+
108
+ this.listeners = [
109
+ () => videoElement.removeEventListener('waiting', onWaiting),
110
+ () => videoElement.removeEventListener('playing', onPlaying),
111
+ () => videoElement.removeEventListener('error', onError),
112
+ ];
113
+
114
+ // Setup unload handler for reliable final report
115
+ const onUnload = () => this.flushSync();
116
+ window.addEventListener('beforeunload', onUnload);
117
+ window.addEventListener('pagehide', onUnload);
118
+ this.listeners.push(
119
+ () => window.removeEventListener('beforeunload', onUnload),
120
+ () => window.removeEventListener('pagehide', onUnload)
121
+ );
122
+
123
+ // Start reporting interval
124
+ this.intervalId = setInterval(() => this.report(), this.config.interval);
125
+
126
+ // Take initial report
127
+ this.report();
128
+ }
129
+
130
+ /**
131
+ * Stop telemetry reporting
132
+ */
133
+ stop(): void {
134
+ // Final report before stopping
135
+ this.flushSync();
136
+
137
+ if (this.intervalId) {
138
+ clearInterval(this.intervalId);
139
+ this.intervalId = null;
140
+ }
141
+
142
+ this.listeners.forEach(cleanup => cleanup());
143
+ this.listeners = [];
144
+
145
+ this.videoElement = null;
146
+ this.qualityGetter = null;
147
+ }
148
+
149
+ /**
150
+ * Record a custom error
151
+ */
152
+ recordError(code: string, message: string): void {
153
+ this.errors.push({
154
+ code,
155
+ message,
156
+ timestamp: Date.now(),
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Generate telemetry payload
162
+ */
163
+ private generatePayload(): TelemetryPayload | null {
164
+ const video = this.videoElement;
165
+ if (!video) return null;
166
+
167
+ // Get quality metrics if available
168
+ const quality = this.qualityGetter?.() ?? null;
169
+
170
+ // Get frame stats if available
171
+ let framesDecoded = 0;
172
+ let framesDropped = 0;
173
+
174
+ if ('getVideoPlaybackQuality' in video) {
175
+ const stats = video.getVideoPlaybackQuality();
176
+ framesDecoded = stats.totalVideoFrames;
177
+ framesDropped = stats.droppedVideoFrames;
178
+ }
179
+
180
+ // Calculate buffered seconds
181
+ let bufferedSeconds = 0;
182
+ if (video.buffered.length > 0) {
183
+ for (let i = 0; i < video.buffered.length; i++) {
184
+ if (video.buffered.start(i) <= video.currentTime && video.buffered.end(i) > video.currentTime) {
185
+ bufferedSeconds = video.buffered.end(i) - video.currentTime;
186
+ break;
187
+ }
188
+ }
189
+ }
190
+
191
+ return {
192
+ timestamp: Date.now(),
193
+ sessionId: this.sessionId,
194
+ contentId: this.config.contentId,
195
+ contentType: this.config.contentType,
196
+ metrics: {
197
+ currentTime: video.currentTime,
198
+ duration: isFinite(video.duration) ? video.duration : -1,
199
+ bufferedSeconds,
200
+ stallCount: this.stallCount,
201
+ totalStallMs: this.totalStallMs,
202
+ bitrate: quality?.bitrate ?? 0,
203
+ qualityScore: quality?.score ?? 100,
204
+ framesDecoded,
205
+ framesDropped,
206
+ playerType: this.config.playerType,
207
+ protocol: this.config.protocol,
208
+ resolution: video.videoWidth > 0 ? {
209
+ width: video.videoWidth,
210
+ height: video.videoHeight,
211
+ } : undefined,
212
+ },
213
+ errors: this.errors.length > 0 ? [...this.errors] : undefined,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Send telemetry report
219
+ */
220
+ private async report(): Promise<void> {
221
+ const payload = this.generatePayload();
222
+ if (!payload) return;
223
+
224
+ this.pendingPayloads.push(payload);
225
+
226
+ // Flush if batch size reached
227
+ if (this.pendingPayloads.length >= this.config.batchSize) {
228
+ await this.flush();
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Flush pending payloads (async)
234
+ */
235
+ private async flush(): Promise<void> {
236
+ if (this.pendingPayloads.length === 0) return;
237
+
238
+ const payloads = [...this.pendingPayloads];
239
+ this.pendingPayloads = [];
240
+
241
+ try {
242
+ const headers: HeadersInit = {
243
+ 'Content-Type': 'application/json',
244
+ };
245
+
246
+ if (this.config.authToken) {
247
+ headers['Authorization'] = `Bearer ${this.config.authToken}`;
248
+ }
249
+
250
+ const response = await fetch(this.config.endpoint, {
251
+ method: 'POST',
252
+ headers,
253
+ body: JSON.stringify(payloads.length === 1 ? payloads[0] : payloads),
254
+ });
255
+
256
+ if (!response.ok) {
257
+ console.warn('[TelemetryReporter] Report failed:', response.status);
258
+ // Re-queue failed payloads (up to a limit)
259
+ if (this.pendingPayloads.length < 10) {
260
+ this.pendingPayloads.unshift(...payloads);
261
+ }
262
+ } else {
263
+ // Clear reported errors
264
+ this.errors = [];
265
+ }
266
+ } catch (error) {
267
+ console.warn('[TelemetryReporter] Report error:', error);
268
+ // Re-queue failed payloads
269
+ if (this.pendingPayloads.length < 10) {
270
+ this.pendingPayloads.unshift(...payloads);
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Flush synchronously using sendBeacon (for page unload)
277
+ */
278
+ private flushSync(): void {
279
+ const payload = this.generatePayload();
280
+ if (!payload) return;
281
+
282
+ const payloads = [...this.pendingPayloads, payload];
283
+ this.pendingPayloads = [];
284
+
285
+ try {
286
+ const data = JSON.stringify(payloads.length === 1 ? payloads[0] : payloads);
287
+ navigator.sendBeacon(this.config.endpoint, new Blob([data], { type: 'application/json' }));
288
+ } catch (error) {
289
+ console.warn('[TelemetryReporter] Beacon failed:', error);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Get session ID
295
+ */
296
+ getSessionId(): string {
297
+ return this.sessionId;
298
+ }
299
+
300
+ /**
301
+ * Check if reporting is active
302
+ */
303
+ isActive(): boolean {
304
+ return this.intervalId !== null;
305
+ }
306
+ }
307
+
308
+ export default TelemetryReporter;