@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,643 @@
1
+ import { BasePlayer } from '../core/PlayerInterface';
2
+ import { LiveDurationProxy } from '../core/LiveDurationProxy';
3
+ import type { StreamSource, StreamInfo, PlayerOptions, PlayerCapability } from '../core/PlayerInterface';
4
+
5
+ export class VideoJsPlayerImpl extends BasePlayer {
6
+ readonly capability: PlayerCapability = {
7
+ name: "Video.js Player",
8
+ shortname: "videojs",
9
+ priority: 2,
10
+ // VideoJS only has built-in HLS support via VHS (videojs-http-streaming)
11
+ // DASH requires videojs-contrib-dash plugin which wraps dash.js - we use DashJsPlayer directly instead
12
+ mimes: [
13
+ "html5/application/vnd.apple.mpegurl",
14
+ "html5/application/vnd.apple.mpegurl;version=7",
15
+ ]
16
+ };
17
+
18
+ private videojsPlayer: any = null;
19
+ private container: HTMLElement | null = null;
20
+ private destroyed = false;
21
+ private timeCorrection: number = 0;
22
+ private proxyElement: HTMLVideoElement | null = null;
23
+ private currentStreamInfo: StreamInfo | null = null;
24
+ private liveDurationProxy: LiveDurationProxy | null = null;
25
+
26
+ isMimeSupported(mimetype: string): boolean {
27
+ return this.capability.mimes.includes(mimetype);
28
+ }
29
+
30
+ isBrowserSupported(mimetype: string, source: StreamSource, streamInfo: StreamInfo): boolean | string[] {
31
+ // Check for HTTP/HTTPS protocol mismatch
32
+ try {
33
+ const sourceProtocol = new URL(source.url).protocol;
34
+ if (typeof location !== 'undefined' && location.protocol !== sourceProtocol) {
35
+ console.debug('[VideoJS] HTTP/HTTPS mismatch - skipping');
36
+ return false;
37
+ }
38
+ } catch {
39
+ // URL parsing failed, continue with other checks
40
+ }
41
+
42
+ // Test codec support properly - don't just assume compatibility
43
+ const playableTracks: string[] = [];
44
+ const tracksByType: Record<string, typeof streamInfo.meta.tracks> = {};
45
+
46
+ // Group tracks by type
47
+ for (const track of streamInfo.meta.tracks) {
48
+ if (track.type === 'meta') {
49
+ if (track.codec === 'subtitle') {
50
+ playableTracks.push('subtitle');
51
+ }
52
+ continue;
53
+ }
54
+
55
+ if (!tracksByType[track.type]) {
56
+ tracksByType[track.type] = [];
57
+ }
58
+ tracksByType[track.type].push(track);
59
+ }
60
+
61
+ // HLS-incompatible audio codecs (VideoJS uses VHS for HLS)
62
+ // HLS standard only supports: AAC, MP3, AC-3/E-AC-3
63
+ const HLS_INCOMPATIBLE_AUDIO = ['OPUS', 'Opus', 'opus', 'VORBIS', 'Vorbis', 'FLAC'];
64
+
65
+ // Test codec support for video/audio tracks using canPlayType
66
+ const testVideo = document.createElement('video');
67
+ for (const [trackType, tracks] of Object.entries(tracksByType)) {
68
+ let hasPlayableTrack = false;
69
+
70
+ for (const track of tracks) {
71
+ // Explicit HLS codec filtering - OPUS doesn't work in HLS even if canPlayType says yes
72
+ if (trackType === 'audio' && HLS_INCOMPATIBLE_AUDIO.includes(track.codec)) {
73
+ console.debug(`[VideoJS] Codec incompatible with HLS: ${track.codec}`);
74
+ continue;
75
+ }
76
+
77
+ // Build codec string
78
+ let codecString = track.codec;
79
+ if (track.init) {
80
+ // Use init data for accurate codec string like HLS.js does
81
+ const bin2hex = (idx: number) => {
82
+ if (!track.init || idx >= track.init.length) return '00';
83
+ return ('0' + track.init.charCodeAt(idx).toString(16)).slice(-2);
84
+ };
85
+ switch (track.codec) {
86
+ case 'H264':
87
+ codecString = `avc1.${bin2hex(1)}${bin2hex(2)}${bin2hex(3)}`;
88
+ break;
89
+ case 'AAC':
90
+ codecString = 'mp4a.40.2';
91
+ break;
92
+ case 'MP3':
93
+ codecString = 'mp4a.40.34';
94
+ break;
95
+ case 'HEVC':
96
+ codecString = 'hev1.1.6.L93.B0';
97
+ break;
98
+ }
99
+ }
100
+
101
+ // Test with video element canPlayType
102
+ const mimeToTest = trackType === 'audio'
103
+ ? `audio/mp4;codecs="${codecString}"`
104
+ : `video/mp4;codecs="${codecString}"`;
105
+
106
+ if (testVideo.canPlayType(mimeToTest) !== '') {
107
+ hasPlayableTrack = true;
108
+ break;
109
+ } else {
110
+ console.debug(`[VideoJS] Codec not supported: ${mimeToTest}`);
111
+ }
112
+ }
113
+
114
+ if (hasPlayableTrack) {
115
+ playableTracks.push(trackType);
116
+ }
117
+ }
118
+
119
+ // If no tracks to test, assume basic support (fallback behavior)
120
+ if (Object.keys(tracksByType).length === 0) {
121
+ return ['video', 'audio'];
122
+ }
123
+
124
+ return playableTracks.length > 0 ? playableTracks : false;
125
+ }
126
+
127
+ async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions, streamInfo?: StreamInfo): Promise<HTMLVideoElement> {
128
+ this.destroyed = false;
129
+ this.container = container;
130
+ this.currentStreamInfo = streamInfo || null;
131
+ container.classList.add('fw-player-container');
132
+
133
+ const video = document.createElement('video');
134
+ video.classList.add('fw-player-video');
135
+ video.setAttribute('playsinline', '');
136
+ video.setAttribute('crossorigin', 'anonymous');
137
+ video.className = 'video-js vjs-default-skin fw-player-video';
138
+
139
+ if (options.autoplay) video.autoplay = true;
140
+ if (options.muted) video.muted = true;
141
+ video.controls = options.controls === true; // Explicit false to hide native controls
142
+ if (options.loop) video.loop = true;
143
+ if (options.poster) video.poster = options.poster;
144
+
145
+ this.videoElement = video;
146
+ container.appendChild(video);
147
+
148
+ this.setupVideoEventListeners(video, options);
149
+
150
+ try {
151
+ const mod = await import('video.js');
152
+ const videojs = (mod as any).default || (mod as any);
153
+
154
+ // When using custom controls (controls: false), disable ALL VideoJS UI elements
155
+ const useVideoJsControls = options.controls === true;
156
+
157
+ // Build VideoJS options
158
+ // NOTE: We disable UI components but NOT children array - that breaks playback
159
+ const vjsOptions: Record<string, any> = {
160
+ autoplay: options.autoplay,
161
+ controls: useVideoJsControls,
162
+ muted: options.muted,
163
+ sources: [{ src: source.url, type: this.getVideoJsType(source.type) }],
164
+ // Disable VideoJS UI components when using custom controls
165
+ loadingSpinner: useVideoJsControls,
166
+ bigPlayButton: useVideoJsControls,
167
+ textTrackDisplay: useVideoJsControls, // We handle subtitles ourselves
168
+ errorDisplay: useVideoJsControls,
169
+ controlBar: useVideoJsControls,
170
+ liveTracker: useVideoJsControls,
171
+ // Don't set children: [] - that can break internal VideoJS components
172
+ };
173
+
174
+ // Android < 7 workaround: enable overrideNative for HLS
175
+ const androidMatch = navigator.userAgent.match(/android\s([\d.]*)/i);
176
+ const androidVersion = androidMatch ? parseFloat(androidMatch[1]) : null;
177
+ if (androidVersion && androidVersion < 7) {
178
+ console.debug('[VideoJS] Android < 7 detected, enabling overrideNative');
179
+ vjsOptions.html5 = { hls: { overrideNative: true } };
180
+ vjsOptions.nativeAudioTracks = false;
181
+ vjsOptions.nativeVideoTracks = false;
182
+ }
183
+
184
+ console.debug('[VideoJS] Creating player with options:', vjsOptions);
185
+ this.videojsPlayer = videojs(video, vjsOptions);
186
+ console.debug('[VideoJS] Player created');
187
+
188
+ // Hide VideoJS UI completely when using custom controls
189
+ if (!useVideoJsControls) {
190
+ // Add class to hide all VideoJS chrome
191
+ const wrapper = this.videojsPlayer.el();
192
+ if (wrapper) {
193
+ wrapper.classList.add('vjs-fw-custom-controls');
194
+ }
195
+ }
196
+
197
+ // Error handling with Firefox NS_ERROR detection
198
+ this.videojsPlayer.on('error', () => {
199
+ if (this.destroyed) return; // Guard against zombie callbacks
200
+ const err = this.videojsPlayer?.error();
201
+ const errorMsg = err?.message || '';
202
+
203
+ // Firefox-specific segment error - trigger reload
204
+ if (errorMsg.includes('NS_ERROR_DOM_MEDIA_OVERFLOW_ERR')) {
205
+ console.warn('[VideoJS] Firefox segment error, requesting reload');
206
+ this.emit('reloadrequested', { reason: 'NS_ERROR_DOM_MEDIA_OVERFLOW_ERR' });
207
+ return;
208
+ }
209
+
210
+ this.emit('error', errorMsg || 'VideoJS playback error');
211
+ });
212
+
213
+ // FIX: Explicitly trigger play after VideoJS is ready
214
+ // VideoJS autoplay option alone doesn't always work (browser policies)
215
+ this.videojsPlayer.ready(() => {
216
+ if (this.destroyed) return; // Guard against zombie callbacks
217
+
218
+ // Debug: Log VideoJS tech info
219
+ const tech = this.videojsPlayer.tech?.({ IWillNotUseThisInPlugins: true });
220
+ console.debug('[VideoJS] ready - tech:', tech?.name || 'unknown',
221
+ 'videoWidth:', video.videoWidth,
222
+ 'videoHeight:', video.videoHeight,
223
+ 'readyState:', video.readyState,
224
+ 'networkState:', video.networkState);
225
+
226
+ // Create time-corrected proxy for external consumers
227
+ if (this.currentStreamInfo) {
228
+ this.proxyElement = this.createTimeCorrectedProxy(video, this.currentStreamInfo);
229
+ }
230
+
231
+ // Check if live stream and set up LiveDurationProxy
232
+ const duration = this.videojsPlayer.duration();
233
+ if (!isFinite(duration) && !this.liveDurationProxy) {
234
+ this.liveDurationProxy = new LiveDurationProxy(video, {
235
+ constrainSeek: true,
236
+ liveOffset: 0,
237
+ });
238
+ console.debug('[VideoJS] LiveDurationProxy initialized for live stream');
239
+ }
240
+
241
+ if (options.autoplay) {
242
+ // Ensure muted for autoplay - browsers block unmuted autoplay
243
+ if (!video.muted) {
244
+ video.muted = true;
245
+ }
246
+ this.videojsPlayer.play().catch((e: any) => {
247
+ console.warn('VideoJS autoplay failed:', e);
248
+ // Emit a warning but don't fail - user can click play
249
+ });
250
+ }
251
+ });
252
+
253
+ // Listen for VideoJS loadedmetadata to track loading progress
254
+ this.videojsPlayer.on('loadedmetadata', () => {
255
+ console.debug('[VideoJS] loadedmetadata - duration:', this.videojsPlayer.duration(),
256
+ 'videoWidth:', video.videoWidth, 'videoHeight:', video.videoHeight);
257
+ });
258
+
259
+ // Debug: Track VHS (video.js http-streaming) state
260
+ this.videojsPlayer.on('loadeddata', () => {
261
+ const tech = this.videojsPlayer.tech?.({ IWillNotUseThisInPlugins: true });
262
+ const vhs = tech?.vhs || tech?.hls;
263
+ if (vhs) {
264
+ console.debug('[VideoJS] VHS state -',
265
+ 'bandwidth:', vhs.bandwidth,
266
+ 'seekable:', vhs.seekable?.()?.length > 0 ? `${vhs.seekable().start(0)}-${vhs.seekable().end(0)}` : 'none',
267
+ 'buffered:', video.buffered.length > 0 ? `${video.buffered.end(0)}s` : 'none');
268
+ }
269
+ });
270
+
271
+ // Listen for canplay from VideoJS to ensure we transition out of buffering
272
+ this.videojsPlayer.on('canplay', () => {
273
+ console.debug('[VideoJS] canplay');
274
+ });
275
+
276
+ // Additional debug events
277
+ this.videojsPlayer.on('playing', () => {
278
+ console.debug('[VideoJS] playing - currentTime:', this.videojsPlayer.currentTime());
279
+ });
280
+
281
+ this.videojsPlayer.on('waiting', () => {
282
+ console.debug('[VideoJS] waiting/buffering');
283
+ });
284
+
285
+ this.videojsPlayer.on('stalled', () => {
286
+ console.debug('[VideoJS] stalled');
287
+ });
288
+
289
+ // Log video element state
290
+ video.addEventListener('loadeddata', () => {
291
+ console.debug('[VideoJS] video loadeddata - readyState:', video.readyState,
292
+ 'videoWidth:', video.videoWidth, 'videoHeight:', video.videoHeight);
293
+ });
294
+
295
+ // Emit seekable range updates for live streams (DVR support)
296
+ this.videojsPlayer.on('progress', () => {
297
+ if (this.destroyed) return;
298
+ try {
299
+ const seekable = this.videojsPlayer.seekable();
300
+ if (seekable && seekable.length > 0) {
301
+ const start = seekable.start(0);
302
+ const end = seekable.end(seekable.length - 1);
303
+ const bufferWindow = (end - start) * 1000; // Convert to ms
304
+ this.emit('seekablechange', {
305
+ start: start + this.timeCorrection,
306
+ end: end + this.timeCorrection,
307
+ bufferWindow
308
+ });
309
+ }
310
+ } catch {
311
+ // Seekable not available yet
312
+ }
313
+ });
314
+
315
+ return this.proxyElement || video;
316
+
317
+ } catch (error: any) {
318
+ this.emit('error', error.message || String(error));
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Creates a Proxy wrapper around the video element that corrects
325
+ * currentTime/duration/buffered using the firstms offset from MistServer.
326
+ * This ensures timestamps align with MistServer's track metadata.
327
+ */
328
+ private createTimeCorrectedProxy(video: HTMLVideoElement, streamInfo: StreamInfo): HTMLVideoElement {
329
+ // Calculate correction from minimum firstms across all tracks
330
+ let firstms = Infinity;
331
+ for (const track of streamInfo.meta.tracks) {
332
+ if ((track as any).firstms !== undefined && (track as any).firstms < firstms) {
333
+ firstms = (track as any).firstms;
334
+ }
335
+ }
336
+ this.timeCorrection = firstms !== Infinity ? firstms / 1000 : 0;
337
+
338
+ // No correction needed or Proxy not supported
339
+ if (this.timeCorrection === 0 || typeof Proxy === 'undefined') {
340
+ return video;
341
+ }
342
+
343
+ console.debug(`[VideoJS] Applying timestamp correction: ${this.timeCorrection}s (firstms=${firstms})`);
344
+
345
+ const correction = this.timeCorrection;
346
+ const vjsPlayer = this.videojsPlayer;
347
+
348
+ return new Proxy(video, {
349
+ get: (target, prop) => {
350
+ if (prop === 'currentTime') {
351
+ const time = vjsPlayer ? vjsPlayer.currentTime() : target.currentTime;
352
+ return isNaN(time) ? 0 : time + correction;
353
+ }
354
+ if (prop === 'duration') {
355
+ const duration = target.duration;
356
+ return isNaN(duration) ? 0 : duration + correction;
357
+ }
358
+ if (prop === 'buffered') {
359
+ const buffered = target.buffered;
360
+ return {
361
+ length: buffered.length,
362
+ start: (i: number) => buffered.start(i) + correction,
363
+ end: (i: number) => buffered.end(i) + correction,
364
+ };
365
+ }
366
+ const value = target[prop as keyof HTMLVideoElement];
367
+ if (typeof value === 'function') {
368
+ return value.bind(target);
369
+ }
370
+ return value;
371
+ },
372
+ set: (target, prop, value) => {
373
+ if (prop === 'currentTime') {
374
+ // Use VideoJS API for seeking (fixes backwards seeking in HLS)
375
+ const correctedValue = value - correction;
376
+ if (vjsPlayer) {
377
+ vjsPlayer.currentTime(correctedValue);
378
+ } else {
379
+ target.currentTime = correctedValue;
380
+ }
381
+ return true;
382
+ }
383
+ (target as any)[prop] = value;
384
+ return true;
385
+ }
386
+ }) as HTMLVideoElement;
387
+ }
388
+
389
+ private getVideoJsType(mimeType?: string): string {
390
+ if (!mimeType) return 'application/x-mpegURL';
391
+
392
+ // Convert our mime types to VideoJS types
393
+ if (mimeType.includes('mpegurl')) return 'application/x-mpegURL';
394
+ if (mimeType.includes('dash')) return 'application/dash+xml';
395
+ if (mimeType.includes('mp4')) return 'video/mp4';
396
+ if (mimeType.includes('webm')) return 'video/webm';
397
+
398
+ return mimeType.replace('html5/', '');
399
+ }
400
+
401
+ setPlaybackRate(rate: number): void {
402
+ super.setPlaybackRate(rate);
403
+ try {
404
+ if (this.videojsPlayer) this.videojsPlayer.playbackRate(rate);
405
+ } catch {}
406
+ }
407
+
408
+ getCurrentTime(): number {
409
+ const v = this.proxyElement || this.videoElement;
410
+ return v?.currentTime ?? 0;
411
+ }
412
+
413
+ getDuration(): number {
414
+ const v = this.proxyElement || this.videoElement;
415
+ return v?.duration ?? 0;
416
+ }
417
+
418
+ getSeekableRange(): { start: number; end: number } | null {
419
+ if (this.videojsPlayer?.seekable) {
420
+ try {
421
+ const seekable = this.videojsPlayer.seekable();
422
+ if (seekable && seekable.length > 0) {
423
+ const start = seekable.start(0) + this.timeCorrection;
424
+ const end = seekable.end(seekable.length - 1) + this.timeCorrection;
425
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
426
+ return { start, end };
427
+ }
428
+ }
429
+ } catch {}
430
+ }
431
+ return null;
432
+ }
433
+
434
+ /**
435
+ * Seek to time using VideoJS API (fixes backwards seeking in HLS).
436
+ * Time should be in the corrected coordinate space (with firstms offset applied).
437
+ */
438
+ seek(time: number): void {
439
+ const correctedTime = time - this.timeCorrection;
440
+ if (this.videojsPlayer) {
441
+ this.videojsPlayer.currentTime(correctedTime);
442
+ } else if (this.videoElement) {
443
+ this.videoElement.currentTime = correctedTime;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Get VideoJS-specific stats for playback monitoring
449
+ */
450
+ async getStats(): Promise<{
451
+ type: 'videojs';
452
+ buffered: number;
453
+ currentTime: number;
454
+ duration: number;
455
+ readyState: number;
456
+ networkState: number;
457
+ playbackRate: number;
458
+ } | undefined> {
459
+ const video = this.videoElement;
460
+ if (!video) return undefined;
461
+
462
+ // Calculate buffered ahead of current position
463
+ let buffered = 0;
464
+ if (video.buffered.length > 0) {
465
+ for (let i = 0; i < video.buffered.length; i++) {
466
+ if (video.buffered.start(i) <= video.currentTime && video.buffered.end(i) > video.currentTime) {
467
+ buffered = video.buffered.end(i) - video.currentTime;
468
+ break;
469
+ }
470
+ }
471
+ }
472
+
473
+ return {
474
+ type: 'videojs',
475
+ buffered,
476
+ currentTime: video.currentTime,
477
+ duration: video.duration,
478
+ readyState: video.readyState,
479
+ networkState: video.networkState,
480
+ playbackRate: video.playbackRate,
481
+ };
482
+ }
483
+
484
+ // ============================================================================
485
+ // Live Stream Support
486
+ // ============================================================================
487
+
488
+ /**
489
+ * Check if the stream is live
490
+ */
491
+ isLiveStream(): boolean {
492
+ if (this.liveDurationProxy) {
493
+ return this.liveDurationProxy.isLive();
494
+ }
495
+ const video = this.videoElement;
496
+ if (!video) return false;
497
+ return !isFinite(video.duration);
498
+ }
499
+
500
+ /**
501
+ * Get the calculated duration for live streams
502
+ */
503
+ getDuration(): number {
504
+ if (this.liveDurationProxy) {
505
+ return this.liveDurationProxy.getDuration();
506
+ }
507
+ return this.videoElement?.duration ?? 0;
508
+ }
509
+
510
+ /**
511
+ * Jump to live edge
512
+ * Uses VideoJS liveTracker when available, otherwise LiveDurationProxy
513
+ */
514
+ jumpToLive(): void {
515
+ const video = this.videoElement;
516
+ if (!video) return;
517
+
518
+ // VideoJS has a liveTracker module for live streams
519
+ if (this.videojsPlayer && this.videojsPlayer.liveTracker) {
520
+ const tracker = this.videojsPlayer.liveTracker;
521
+ if (tracker.isLive && tracker.isLive()) {
522
+ const liveCurrentTime = tracker.liveCurrentTime?.();
523
+ if (typeof liveCurrentTime === 'number' && liveCurrentTime > 0) {
524
+ console.debug('[VideoJS] jumpToLive using liveTracker:', liveCurrentTime);
525
+ this.videojsPlayer.currentTime(liveCurrentTime);
526
+ return;
527
+ }
528
+ }
529
+ }
530
+
531
+ // Fall back to LiveDurationProxy
532
+ if (this.liveDurationProxy && this.liveDurationProxy.isLive()) {
533
+ console.debug('[VideoJS] jumpToLive using LiveDurationProxy');
534
+ this.liveDurationProxy.jumpToLive();
535
+ return;
536
+ }
537
+
538
+ // VideoJS seekable fallback
539
+ if (this.videojsPlayer) {
540
+ try {
541
+ const seekable = this.videojsPlayer.seekable();
542
+ if (seekable && seekable.length > 0) {
543
+ const liveEdge = seekable.end(seekable.length - 1);
544
+ if (isFinite(liveEdge) && liveEdge > 0) {
545
+ console.debug('[VideoJS] jumpToLive using seekable.end:', liveEdge);
546
+ this.videojsPlayer.currentTime(liveEdge);
547
+ return;
548
+ }
549
+ }
550
+ } catch {}
551
+ }
552
+
553
+ // Native video seekable fallback
554
+ if (video.seekable && video.seekable.length > 0) {
555
+ const liveEdge = video.seekable.end(video.seekable.length - 1);
556
+ if (isFinite(liveEdge) && liveEdge > 0) {
557
+ console.debug('[VideoJS] jumpToLive using video.seekable.end:', liveEdge);
558
+ video.currentTime = liveEdge;
559
+ }
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Provide a seekable range override for live streams.
565
+ * Uses VideoJS liveTracker seekableEnd as the live edge when available.
566
+ */
567
+ getSeekableRange(): { start: number; end: number } | null {
568
+ const video = this.videoElement;
569
+ if (!video?.seekable || video.seekable.length === 0) return null;
570
+ let start = video.seekable.start(0);
571
+ let end = video.seekable.end(video.seekable.length - 1);
572
+
573
+ if (this.videojsPlayer?.liveTracker) {
574
+ const tracker = this.videojsPlayer.liveTracker;
575
+ const trackerEnd = tracker.seekableEnd?.();
576
+ const trackerStart = tracker.seekableStart?.();
577
+ if (typeof trackerStart === 'number' && Number.isFinite(trackerStart)) {
578
+ start = trackerStart;
579
+ }
580
+ if (typeof trackerEnd === 'number' && Number.isFinite(trackerEnd) && trackerEnd > 0) {
581
+ end = Math.min(end, trackerEnd);
582
+ }
583
+ }
584
+
585
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
586
+ return { start, end };
587
+ }
588
+
589
+ /**
590
+ * Get latency from live edge (for live streams)
591
+ */
592
+ getLiveLatency(): number {
593
+ const video = this.videoElement;
594
+ if (!video) return 0;
595
+
596
+ // VideoJS liveTracker provides seekableEnd
597
+ if (this.videojsPlayer && this.videojsPlayer.liveTracker) {
598
+ const tracker = this.videojsPlayer.liveTracker;
599
+ if (tracker.isLive?.() && typeof tracker.seekableEnd === 'function') {
600
+ const liveEdge = tracker.seekableEnd();
601
+ if (typeof liveEdge === 'number' && isFinite(liveEdge)) {
602
+ return Math.max(0, (liveEdge - video.currentTime) * 1000);
603
+ }
604
+ }
605
+ }
606
+
607
+ // Fall back to proxy
608
+ if (this.liveDurationProxy) {
609
+ return this.liveDurationProxy.getLatency() * 1000;
610
+ }
611
+
612
+ return 0;
613
+ }
614
+
615
+ async destroy(): Promise<void> {
616
+ this.destroyed = true;
617
+
618
+ if (this.liveDurationProxy) {
619
+ this.liveDurationProxy.destroy();
620
+ this.liveDurationProxy = null;
621
+ }
622
+
623
+ if (this.videojsPlayer) {
624
+ try {
625
+ this.videojsPlayer.dispose();
626
+ } catch (e) {
627
+ console.warn('Error disposing VideoJS:', e);
628
+ }
629
+ this.videojsPlayer = null;
630
+ }
631
+
632
+ if (this.videoElement && this.container) {
633
+ try { this.container.removeChild(this.videoElement); } catch {}
634
+ }
635
+
636
+ this.videoElement = null;
637
+ this.container = null;
638
+ this.proxyElement = null;
639
+ this.currentStreamInfo = null;
640
+ this.timeCorrection = 0;
641
+ this.listeners.clear();
642
+ }
643
+ }