@livepeer-frameworks/player-core 0.1.1 → 0.1.2
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/core/ABRController.js +456 -0
- package/dist/cjs/core/ABRController.js.map +1 -0
- package/dist/cjs/core/CodecUtils.js +195 -0
- package/dist/cjs/core/CodecUtils.js.map +1 -0
- package/dist/cjs/core/ErrorClassifier.js +410 -0
- package/dist/cjs/core/ErrorClassifier.js.map +1 -0
- package/dist/cjs/core/EventEmitter.js +108 -0
- package/dist/cjs/core/EventEmitter.js.map +1 -0
- package/dist/cjs/core/GatewayClient.js +342 -0
- package/dist/cjs/core/GatewayClient.js.map +1 -0
- package/dist/cjs/core/InteractionController.js +606 -0
- package/dist/cjs/core/InteractionController.js.map +1 -0
- package/dist/cjs/core/LiveDurationProxy.js +186 -0
- package/dist/cjs/core/LiveDurationProxy.js.map +1 -0
- package/dist/cjs/core/MetaTrackManager.js +624 -0
- package/dist/cjs/core/MetaTrackManager.js.map +1 -0
- package/dist/cjs/core/MistReporter.js +449 -0
- package/dist/cjs/core/MistReporter.js.map +1 -0
- package/dist/cjs/core/MistSignaling.js +264 -0
- package/dist/cjs/core/MistSignaling.js.map +1 -0
- package/dist/cjs/core/PlayerController.js +2658 -0
- package/dist/cjs/core/PlayerController.js.map +1 -0
- package/dist/cjs/core/PlayerInterface.js +269 -0
- package/dist/cjs/core/PlayerInterface.js.map +1 -0
- package/dist/cjs/core/PlayerManager.js +806 -0
- package/dist/cjs/core/PlayerManager.js.map +1 -0
- package/dist/cjs/core/PlayerRegistry.js +270 -0
- package/dist/cjs/core/PlayerRegistry.js.map +1 -0
- package/dist/cjs/core/QualityMonitor.js +474 -0
- package/dist/cjs/core/QualityMonitor.js.map +1 -0
- package/dist/cjs/core/SeekingUtils.js +292 -0
- package/dist/cjs/core/SeekingUtils.js.map +1 -0
- package/dist/cjs/core/StreamStateClient.js +381 -0
- package/dist/cjs/core/StreamStateClient.js.map +1 -0
- package/dist/cjs/core/SubtitleManager.js +227 -0
- package/dist/cjs/core/SubtitleManager.js.map +1 -0
- package/dist/cjs/core/TelemetryReporter.js +258 -0
- package/dist/cjs/core/TelemetryReporter.js.map +1 -0
- package/dist/cjs/core/TimeFormat.js +176 -0
- package/dist/cjs/core/TimeFormat.js.map +1 -0
- package/dist/cjs/core/TimerManager.js +176 -0
- package/dist/cjs/core/TimerManager.js.map +1 -0
- package/dist/cjs/core/UrlUtils.js +160 -0
- package/dist/cjs/core/UrlUtils.js.map +1 -0
- package/dist/cjs/core/detector.js +293 -0
- package/dist/cjs/core/detector.js.map +1 -0
- package/dist/cjs/core/scorer.js +443 -0
- package/dist/cjs/core/scorer.js.map +1 -0
- package/dist/cjs/index.js +121 -20134
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/utils.js +11 -0
- package/dist/cjs/lib/utils.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +6 -0
- package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3042 -0
- package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/cjs/players/DashJsPlayer.js +638 -0
- package/dist/cjs/players/DashJsPlayer.js.map +1 -0
- package/dist/cjs/players/HlsJsPlayer.js +482 -0
- package/dist/cjs/players/HlsJsPlayer.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js +522 -0
- package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js +215 -0
- package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/cjs/players/MewsWsPlayer/index.js +987 -0
- package/dist/cjs/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/cjs/players/MistPlayer.js +185 -0
- package/dist/cjs/players/MistPlayer.js.map +1 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js +635 -0
- package/dist/cjs/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/cjs/players/NativePlayer.js +762 -0
- package/dist/cjs/players/NativePlayer.js.map +1 -0
- package/dist/cjs/players/VideoJsPlayer.js +585 -0
- package/dist/cjs/players/VideoJsPlayer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js +236 -0
- package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js +143 -0
- package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js +96 -0
- package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js +359 -0
- package/dist/cjs/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js +460 -0
- package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js +1467 -0
- package/dist/cjs/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +320 -0
- package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/cjs/styles/index.js +57 -0
- package/dist/cjs/styles/index.js.map +1 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js +269 -0
- package/dist/cjs/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/cjs/vanilla.js +11 -0
- package/dist/cjs/vanilla.js.map +1 -0
- package/dist/esm/core/ABRController.js +454 -0
- package/dist/esm/core/ABRController.js.map +1 -0
- package/dist/esm/core/CodecUtils.js +193 -0
- package/dist/esm/core/CodecUtils.js.map +1 -0
- package/dist/esm/core/ErrorClassifier.js +408 -0
- package/dist/esm/core/ErrorClassifier.js.map +1 -0
- package/dist/esm/core/EventEmitter.js +106 -0
- package/dist/esm/core/EventEmitter.js.map +1 -0
- package/dist/esm/core/GatewayClient.js +340 -0
- package/dist/esm/core/GatewayClient.js.map +1 -0
- package/dist/esm/core/InteractionController.js +604 -0
- package/dist/esm/core/InteractionController.js.map +1 -0
- package/dist/esm/core/LiveDurationProxy.js +184 -0
- package/dist/esm/core/LiveDurationProxy.js.map +1 -0
- package/dist/esm/core/MetaTrackManager.js +622 -0
- package/dist/esm/core/MetaTrackManager.js.map +1 -0
- package/dist/esm/core/MistReporter.js +447 -0
- package/dist/esm/core/MistReporter.js.map +1 -0
- package/dist/esm/core/MistSignaling.js +262 -0
- package/dist/esm/core/MistSignaling.js.map +1 -0
- package/dist/esm/core/PlayerController.js +2651 -0
- package/dist/esm/core/PlayerController.js.map +1 -0
- package/dist/esm/core/PlayerInterface.js +267 -0
- package/dist/esm/core/PlayerInterface.js.map +1 -0
- package/dist/esm/core/PlayerManager.js +804 -0
- package/dist/esm/core/PlayerManager.js.map +1 -0
- package/dist/esm/core/PlayerRegistry.js +264 -0
- package/dist/esm/core/PlayerRegistry.js.map +1 -0
- package/dist/esm/core/QualityMonitor.js +471 -0
- package/dist/esm/core/QualityMonitor.js.map +1 -0
- package/dist/esm/core/SeekingUtils.js +280 -0
- package/dist/esm/core/SeekingUtils.js.map +1 -0
- package/dist/esm/core/StreamStateClient.js +379 -0
- package/dist/esm/core/StreamStateClient.js.map +1 -0
- package/dist/esm/core/SubtitleManager.js +225 -0
- package/dist/esm/core/SubtitleManager.js.map +1 -0
- package/dist/esm/core/TelemetryReporter.js +256 -0
- package/dist/esm/core/TelemetryReporter.js.map +1 -0
- package/dist/esm/core/TimeFormat.js +169 -0
- package/dist/esm/core/TimeFormat.js.map +1 -0
- package/dist/esm/core/TimerManager.js +174 -0
- package/dist/esm/core/TimerManager.js.map +1 -0
- package/dist/esm/core/UrlUtils.js +151 -0
- package/dist/esm/core/UrlUtils.js.map +1 -0
- package/dist/esm/core/detector.js +279 -0
- package/dist/esm/core/detector.js.map +1 -0
- package/dist/esm/core/scorer.js +422 -0
- package/dist/esm/core/scorer.js.map +1 -0
- package/dist/esm/index.js +26 -20043
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/utils.js +9 -0
- package/dist/esm/lib/utils.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +4 -0
- package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3036 -0
- package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/esm/players/DashJsPlayer.js +636 -0
- package/dist/esm/players/DashJsPlayer.js.map +1 -0
- package/dist/esm/players/HlsJsPlayer.js +480 -0
- package/dist/esm/players/HlsJsPlayer.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js +520 -0
- package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js +213 -0
- package/dist/esm/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
- package/dist/esm/players/MewsWsPlayer/index.js +985 -0
- package/dist/esm/players/MewsWsPlayer/index.js.map +1 -0
- package/dist/esm/players/MistPlayer.js +183 -0
- package/dist/esm/players/MistPlayer.js.map +1 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js +633 -0
- package/dist/esm/players/MistWebRTCPlayer/index.js.map +1 -0
- package/dist/esm/players/NativePlayer.js +759 -0
- package/dist/esm/players/NativePlayer.js.map +1 -0
- package/dist/esm/players/VideoJsPlayer.js +583 -0
- package/dist/esm/players/VideoJsPlayer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js +233 -0
- package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js +134 -0
- package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js +91 -0
- package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js +357 -0
- package/dist/esm/players/WebCodecsPlayer/SyncController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js +458 -0
- package/dist/esm/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/index.js +1458 -0
- package/dist/esm/players/WebCodecsPlayer/index.js.map +1 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +315 -0
- package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
- package/dist/esm/styles/index.js +54 -0
- package/dist/esm/styles/index.js.map +1 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js +264 -0
- package/dist/esm/vanilla/FrameWorksPlayer.js.map +1 -0
- package/dist/esm/vanilla.js +2 -0
- package/dist/esm/vanilla.js.map +1 -0
- package/dist/player.css +4 -1
- package/dist/types/core/ABRController.d.ts +4 -4
- package/dist/types/core/CodecUtils.d.ts +1 -1
- package/dist/types/core/ErrorClassifier.d.ts +77 -0
- package/dist/types/core/GatewayClient.d.ts +4 -4
- package/dist/types/core/MetaTrackManager.d.ts +2 -2
- package/dist/types/core/MistReporter.d.ts +3 -3
- package/dist/types/core/MistSignaling.d.ts +12 -12
- package/dist/types/core/PlayerController.d.ts +19 -14
- package/dist/types/core/PlayerInterface.d.ts +100 -2
- package/dist/types/core/PlayerManager.d.ts +36 -9
- package/dist/types/core/PlayerRegistry.d.ts +11 -11
- package/dist/types/core/QualityMonitor.d.ts +2 -2
- package/dist/types/core/SeekingUtils.d.ts +2 -2
- package/dist/types/core/StreamStateClient.d.ts +2 -2
- package/dist/types/core/TelemetryReporter.d.ts +1 -1
- package/dist/types/core/TimerManager.d.ts +1 -1
- package/dist/types/core/detector.d.ts +1 -1
- package/dist/types/core/index.d.ts +44 -44
- package/dist/types/core/scorer.d.ts +1 -1
- package/dist/types/core/selector.d.ts +2 -2
- package/dist/types/index.d.ts +35 -34
- package/dist/types/players/DashJsPlayer.d.ts +3 -3
- package/dist/types/players/HlsJsPlayer.d.ts +3 -3
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +1 -1
- package/dist/types/players/MewsWsPlayer/index.d.ts +2 -2
- package/dist/types/players/MewsWsPlayer/types.d.ts +15 -15
- package/dist/types/players/MistPlayer.d.ts +2 -2
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +3 -3
- package/dist/types/players/NativePlayer.d.ts +3 -3
- package/dist/types/players/VideoJsPlayer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +2 -2
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +3 -3
- package/dist/types/players/WebCodecsPlayer/index.d.ts +9 -9
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/types.d.ts +49 -49
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +31 -31
- package/dist/types/players/index.d.ts +5 -8
- package/dist/types/types.d.ts +15 -15
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +2 -2
- package/dist/types/vanilla/index.d.ts +4 -4
- package/dist/workers/decoder.worker.js +129 -122
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +31 -15
- package/src/core/ErrorClassifier.ts +499 -0
- package/src/core/PlayerController.ts +17 -2
- package/src/core/PlayerInterface.ts +109 -0
- package/src/core/PlayerManager.ts +290 -46
- package/src/core/PlayerRegistry.ts +221 -87
- package/src/core/TelemetryReporter.ts +4 -1
- package/src/index.ts +13 -4
- package/src/players/WebCodecsPlayer/index.ts +2 -2
- package/src/players/index.ts +5 -16
- package/src/styles/player.css +4 -1
- package/src/vanilla/FrameWorksPlayer.ts +2 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubtitleManager.js","sources":["../../../../src/core/SubtitleManager.ts"],"sourcesContent":["/**\n * SubtitleManager - WebVTT subtitle track management\n *\n * Based on MistMetaPlayer's subtitle handling (wrappers/html5.js, webrtc.js).\n * Manages text tracks on video elements with support for:\n * - Loading WebVTT from MistServer URLs\n * - Multiple subtitle track selection\n * - Sync correction for WebRTC seek offsets\n */\n\nexport interface SubtitleTrackInfo {\n /** Track ID (from MistServer) */\n id: string;\n /** Display label */\n label: string;\n /** Language code (e.g., 'en', 'es') */\n lang: string;\n /** Source URL for WebVTT file */\n src: string;\n /** Whether this is the default track */\n default?: boolean;\n}\n\nexport interface SubtitleManagerConfig {\n /** Base URL for MistServer (for constructing track URLs) */\n mistBaseUrl?: string;\n /** Stream name */\n streamName?: string;\n /** URL append string (auth tokens, etc.) */\n urlAppend?: string;\n /** Debug logging */\n debug?: boolean;\n}\n\n/**\n * SubtitleManager handles text track lifecycle on a video element\n */\nexport class SubtitleManager {\n private video: HTMLVideoElement | null = null;\n private config: SubtitleManagerConfig;\n private currentTrackId: string | null = null;\n private seekOffset = 0;\n private debug: boolean;\n private listeners: Array<() => void> = [];\n\n constructor(config: SubtitleManagerConfig = {}) {\n this.config = config;\n this.debug = config.debug ?? false;\n }\n\n /**\n * Attach to a video element\n */\n attach(video: HTMLVideoElement): void {\n this.detach();\n this.video = video;\n\n // Listen for events that may require sync correction\n const onLoadedData = () => this.correctSubtitleSync();\n const onSeeked = () => this.correctSubtitleSync();\n\n video.addEventListener(\"loadeddata\", onLoadedData);\n video.addEventListener(\"seeked\", onSeeked);\n\n this.listeners = [\n () => video.removeEventListener(\"loadeddata\", onLoadedData),\n () => video.removeEventListener(\"seeked\", onSeeked),\n ];\n }\n\n /**\n * Detach from video element\n */\n detach(): void {\n this.listeners.forEach((fn) => fn());\n this.listeners = [];\n this.removeAllTracks();\n this.video = null;\n this.currentTrackId = null;\n }\n\n /**\n * Get available text tracks from the video element\n */\n getTextTracks(): TextTrack[] {\n if (!this.video) return [];\n return Array.from(this.video.textTracks);\n }\n\n /**\n * Get all track elements from the video\n */\n getTrackElements(): HTMLTrackElement[] {\n if (!this.video) return [];\n return Array.from(this.video.querySelectorAll(\"track\"));\n }\n\n /**\n * Set the active subtitle track\n * Pass null to disable subtitles\n */\n setSubtitle(track: SubtitleTrackInfo | null): void {\n if (!this.video) {\n this.log(\"Cannot set subtitle: no video element attached\");\n return;\n }\n\n // Remove existing subtitle tracks\n this.removeAllTracks();\n\n if (!track) {\n this.currentTrackId = null;\n this.log(\"Subtitles disabled\");\n return;\n }\n\n // Create new track element\n const trackElement = document.createElement(\"track\");\n trackElement.kind = \"subtitles\";\n trackElement.label = track.label;\n trackElement.srclang = track.lang;\n trackElement.src = this.buildTrackUrl(track.src);\n trackElement.default = true;\n\n // Set up load handler for sync correction\n trackElement.addEventListener(\"load\", () => {\n this.correctSubtitleSync();\n });\n\n this.video.appendChild(trackElement);\n this.currentTrackId = track.id;\n\n // Enable the track\n const textTrack = this.video.textTracks[this.video.textTracks.length - 1];\n if (textTrack) {\n textTrack.mode = \"showing\";\n }\n\n this.log(`Subtitle track set: ${track.label} (${track.lang})`);\n }\n\n /**\n * Build track URL with base URL and append params\n */\n private buildTrackUrl(src: string): string {\n let url = src;\n\n // If relative URL and base URL provided, construct full URL\n if (!url.startsWith(\"http\") && this.config.mistBaseUrl) {\n const base = this.config.mistBaseUrl.replace(/\\/$/, \"\");\n url = url.startsWith(\"/\") ? `${base}${url}` : `${base}/${url}`;\n }\n\n // Append URL params if configured\n if (this.config.urlAppend) {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n url = `${url}${separator}${this.config.urlAppend}`;\n }\n\n return url;\n }\n\n /**\n * Create subtitle track info from MistServer track metadata\n */\n static createTrackInfo(\n trackId: string,\n label: string,\n lang: string,\n baseUrl: string,\n streamName: string\n ): SubtitleTrackInfo {\n // MistServer WebVTT URL format\n const src = `${baseUrl}/${streamName}.vtt?track=${trackId}`;\n return { id: trackId, label, lang, src };\n }\n\n /**\n * Remove all track elements from video\n */\n removeAllTracks(): void {\n if (!this.video) return;\n\n const tracks = this.video.querySelectorAll(\"track\");\n tracks.forEach((track) => track.remove());\n }\n\n /**\n * Get currently active track ID\n */\n getCurrentTrackId(): string | null {\n return this.currentTrackId;\n }\n\n /**\n * Set seek offset for WebRTC sync correction\n * WebRTC playback has a seek offset that needs to be applied to subtitle timing\n */\n setSeekOffset(offset: number): void {\n const oldOffset = this.seekOffset;\n this.seekOffset = offset;\n\n // Re-sync if offset changed significantly\n if (Math.abs(oldOffset - offset) > 1) {\n this.correctSubtitleSync();\n }\n }\n\n /**\n * Correct subtitle timing based on seek offset\n * This is needed for WebRTC where video.currentTime doesn't match actual playback position\n */\n private correctSubtitleSync(): void {\n if (!this.video || this.video.textTracks.length === 0) return;\n\n const textTrack = this.video.textTracks[0];\n if (!textTrack || !textTrack.cues) return;\n\n const currentOffset = (textTrack as any).currentOffset || 0;\n\n // Don't bother if change is small\n if (Math.abs(this.seekOffset - currentOffset) < 1) return;\n\n this.log(`Correcting subtitle sync: offset ${currentOffset} -> ${this.seekOffset}`);\n\n // Collect and re-add cues with corrected timing\n const newCues: VTTCue[] = [];\n\n for (let i = textTrack.cues.length - 1; i >= 0; i--) {\n const cue = textTrack.cues[i] as VTTCue;\n textTrack.removeCue(cue);\n\n // Store original timing if not already stored\n if (!(cue as any).orig) {\n (cue as any).orig = { start: cue.startTime, end: cue.endTime };\n }\n\n // Apply offset correction\n cue.startTime = (cue as any).orig.start - this.seekOffset;\n cue.endTime = (cue as any).orig.end - this.seekOffset;\n\n newCues.push(cue);\n }\n\n // Re-add cues\n for (const cue of newCues) {\n try {\n textTrack.addCue(cue);\n } catch {\n // Ignore errors from invalid cue timing\n }\n }\n\n (textTrack as any).currentOffset = this.seekOffset;\n }\n\n /**\n * Parse subtitle tracks from MistServer stream info\n */\n static parseTracksFromStreamInfo(\n streamInfo: {\n meta?: { tracks?: Record<string, { type: string; codec: string; lang?: string }> };\n },\n baseUrl: string,\n streamName: string\n ): SubtitleTrackInfo[] {\n const tracks: SubtitleTrackInfo[] = [];\n\n if (!streamInfo.meta?.tracks) return tracks;\n\n for (const [trackId, trackData] of Object.entries(streamInfo.meta.tracks)) {\n if (trackData.type === \"meta\" && trackData.codec === \"subtitle\") {\n const lang = trackData.lang || \"und\";\n const label = lang === \"und\" ? `Subtitles ${trackId}` : lang.toUpperCase();\n tracks.push(SubtitleManager.createTrackInfo(trackId, label, lang, baseUrl, streamName));\n }\n }\n\n return tracks;\n }\n\n /**\n * Debug logging\n */\n private log(message: string): void {\n if (this.debug) {\n console.debug(`[SubtitleManager] ${message}`);\n }\n }\n\n /**\n * Cleanup\n */\n destroy(): void {\n this.detach();\n }\n}\n\nexport default SubtitleManager;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a unique session ID
|
|
5
|
+
*/
|
|
6
|
+
function generateSessionId() {
|
|
7
|
+
const bytes = new Uint8Array(8);
|
|
8
|
+
crypto.getRandomValues(bytes);
|
|
9
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
return `${Date.now().toString(36)}-${hex}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* TelemetryReporter - Sends playback metrics to server
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Batched reporting at configurable interval
|
|
17
|
+
* - Retry with exponential backoff on failure
|
|
18
|
+
* - Uses navigator.sendBeacon() for reliable page unload reporting
|
|
19
|
+
* - Tracks errors during playback
|
|
20
|
+
*/
|
|
21
|
+
class TelemetryReporter {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.intervalId = null;
|
|
24
|
+
this.pendingPayloads = [];
|
|
25
|
+
this.errors = [];
|
|
26
|
+
this.stallCount = 0;
|
|
27
|
+
this.totalStallMs = 0;
|
|
28
|
+
this.lastStallStart = 0;
|
|
29
|
+
this.videoElement = null;
|
|
30
|
+
this.qualityGetter = null;
|
|
31
|
+
this.listeners = [];
|
|
32
|
+
this.config = {
|
|
33
|
+
endpoint: config.endpoint,
|
|
34
|
+
authToken: config.authToken ?? "",
|
|
35
|
+
interval: config.interval ?? 5000,
|
|
36
|
+
batchSize: config.batchSize ?? 1,
|
|
37
|
+
contentId: config.contentId,
|
|
38
|
+
contentType: config.contentType,
|
|
39
|
+
playerType: config.playerType,
|
|
40
|
+
protocol: config.protocol,
|
|
41
|
+
};
|
|
42
|
+
this.sessionId = generateSessionId();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start telemetry reporting
|
|
46
|
+
*/
|
|
47
|
+
start(videoElement, qualityGetter) {
|
|
48
|
+
this.stop();
|
|
49
|
+
this.videoElement = videoElement;
|
|
50
|
+
this.qualityGetter = qualityGetter ?? null;
|
|
51
|
+
this.stallCount = 0;
|
|
52
|
+
this.totalStallMs = 0;
|
|
53
|
+
this.errors = [];
|
|
54
|
+
// Track stalls
|
|
55
|
+
const onWaiting = () => {
|
|
56
|
+
this.stallCount++;
|
|
57
|
+
this.lastStallStart = performance.now();
|
|
58
|
+
};
|
|
59
|
+
const onPlaying = () => {
|
|
60
|
+
if (this.lastStallStart > 0) {
|
|
61
|
+
this.totalStallMs += performance.now() - this.lastStallStart;
|
|
62
|
+
this.lastStallStart = 0;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const onError = () => {
|
|
66
|
+
const error = videoElement.error;
|
|
67
|
+
if (error) {
|
|
68
|
+
this.errors.push({
|
|
69
|
+
code: String(error.code),
|
|
70
|
+
message: error.message || "Unknown error",
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
videoElement.addEventListener("waiting", onWaiting);
|
|
76
|
+
videoElement.addEventListener("playing", onPlaying);
|
|
77
|
+
videoElement.addEventListener("error", onError);
|
|
78
|
+
this.listeners = [
|
|
79
|
+
() => videoElement.removeEventListener("waiting", onWaiting),
|
|
80
|
+
() => videoElement.removeEventListener("playing", onPlaying),
|
|
81
|
+
() => videoElement.removeEventListener("error", onError),
|
|
82
|
+
];
|
|
83
|
+
// Setup unload handler for reliable final report
|
|
84
|
+
const onUnload = () => this.flushSync();
|
|
85
|
+
window.addEventListener("beforeunload", onUnload);
|
|
86
|
+
window.addEventListener("pagehide", onUnload);
|
|
87
|
+
this.listeners.push(() => window.removeEventListener("beforeunload", onUnload), () => window.removeEventListener("pagehide", onUnload));
|
|
88
|
+
// Start reporting interval
|
|
89
|
+
this.intervalId = setInterval(() => this.report(), this.config.interval);
|
|
90
|
+
// Take initial report
|
|
91
|
+
this.report();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Stop telemetry reporting
|
|
95
|
+
*/
|
|
96
|
+
stop() {
|
|
97
|
+
// Final report before stopping
|
|
98
|
+
this.flushSync();
|
|
99
|
+
if (this.intervalId) {
|
|
100
|
+
clearInterval(this.intervalId);
|
|
101
|
+
this.intervalId = null;
|
|
102
|
+
}
|
|
103
|
+
this.listeners.forEach((cleanup) => cleanup());
|
|
104
|
+
this.listeners = [];
|
|
105
|
+
this.videoElement = null;
|
|
106
|
+
this.qualityGetter = null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Record a custom error
|
|
110
|
+
*/
|
|
111
|
+
recordError(code, message) {
|
|
112
|
+
this.errors.push({
|
|
113
|
+
code,
|
|
114
|
+
message,
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Generate telemetry payload
|
|
120
|
+
*/
|
|
121
|
+
generatePayload() {
|
|
122
|
+
const video = this.videoElement;
|
|
123
|
+
if (!video)
|
|
124
|
+
return null;
|
|
125
|
+
// Get quality metrics if available
|
|
126
|
+
const quality = this.qualityGetter?.() ?? null;
|
|
127
|
+
// Get frame stats if available
|
|
128
|
+
let framesDecoded = 0;
|
|
129
|
+
let framesDropped = 0;
|
|
130
|
+
if ("getVideoPlaybackQuality" in video) {
|
|
131
|
+
const stats = video.getVideoPlaybackQuality();
|
|
132
|
+
framesDecoded = stats.totalVideoFrames;
|
|
133
|
+
framesDropped = stats.droppedVideoFrames;
|
|
134
|
+
}
|
|
135
|
+
// Calculate buffered seconds
|
|
136
|
+
let bufferedSeconds = 0;
|
|
137
|
+
if (video.buffered.length > 0) {
|
|
138
|
+
for (let i = 0; i < video.buffered.length; i++) {
|
|
139
|
+
if (video.buffered.start(i) <= video.currentTime &&
|
|
140
|
+
video.buffered.end(i) > video.currentTime) {
|
|
141
|
+
bufferedSeconds = video.buffered.end(i) - video.currentTime;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
sessionId: this.sessionId,
|
|
149
|
+
contentId: this.config.contentId,
|
|
150
|
+
contentType: this.config.contentType,
|
|
151
|
+
metrics: {
|
|
152
|
+
currentTime: video.currentTime,
|
|
153
|
+
duration: isFinite(video.duration) ? video.duration : -1,
|
|
154
|
+
bufferedSeconds,
|
|
155
|
+
stallCount: this.stallCount,
|
|
156
|
+
totalStallMs: this.totalStallMs,
|
|
157
|
+
bitrate: quality?.bitrate ?? 0,
|
|
158
|
+
qualityScore: quality?.score ?? 100,
|
|
159
|
+
framesDecoded,
|
|
160
|
+
framesDropped,
|
|
161
|
+
playerType: this.config.playerType,
|
|
162
|
+
protocol: this.config.protocol,
|
|
163
|
+
resolution: video.videoWidth > 0
|
|
164
|
+
? {
|
|
165
|
+
width: video.videoWidth,
|
|
166
|
+
height: video.videoHeight,
|
|
167
|
+
}
|
|
168
|
+
: undefined,
|
|
169
|
+
},
|
|
170
|
+
errors: this.errors.length > 0 ? [...this.errors] : undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Send telemetry report
|
|
175
|
+
*/
|
|
176
|
+
async report() {
|
|
177
|
+
const payload = this.generatePayload();
|
|
178
|
+
if (!payload)
|
|
179
|
+
return;
|
|
180
|
+
this.pendingPayloads.push(payload);
|
|
181
|
+
// Flush if batch size reached
|
|
182
|
+
if (this.pendingPayloads.length >= this.config.batchSize) {
|
|
183
|
+
await this.flush();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Flush pending payloads (async)
|
|
188
|
+
*/
|
|
189
|
+
async flush() {
|
|
190
|
+
if (this.pendingPayloads.length === 0)
|
|
191
|
+
return;
|
|
192
|
+
const payloads = [...this.pendingPayloads];
|
|
193
|
+
this.pendingPayloads = [];
|
|
194
|
+
try {
|
|
195
|
+
const headers = {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
};
|
|
198
|
+
if (this.config.authToken) {
|
|
199
|
+
headers["Authorization"] = `Bearer ${this.config.authToken}`;
|
|
200
|
+
}
|
|
201
|
+
const response = await fetch(this.config.endpoint, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers,
|
|
204
|
+
body: JSON.stringify(payloads.length === 1 ? payloads[0] : payloads),
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
console.warn("[TelemetryReporter] Report failed:", response.status);
|
|
208
|
+
// Re-queue failed payloads (up to a limit)
|
|
209
|
+
if (this.pendingPayloads.length < 10) {
|
|
210
|
+
this.pendingPayloads.unshift(...payloads);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Clear reported errors
|
|
215
|
+
this.errors = [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.warn("[TelemetryReporter] Report error:", error);
|
|
220
|
+
// Re-queue failed payloads
|
|
221
|
+
if (this.pendingPayloads.length < 10) {
|
|
222
|
+
this.pendingPayloads.unshift(...payloads);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Flush synchronously using sendBeacon (for page unload)
|
|
228
|
+
*/
|
|
229
|
+
flushSync() {
|
|
230
|
+
const payload = this.generatePayload();
|
|
231
|
+
if (!payload)
|
|
232
|
+
return;
|
|
233
|
+
const payloads = [...this.pendingPayloads, payload];
|
|
234
|
+
this.pendingPayloads = [];
|
|
235
|
+
try {
|
|
236
|
+
const data = JSON.stringify(payloads.length === 1 ? payloads[0] : payloads);
|
|
237
|
+
navigator.sendBeacon(this.config.endpoint, new Blob([data], { type: "application/json" }));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.warn("[TelemetryReporter] Beacon failed:", error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get session ID
|
|
245
|
+
*/
|
|
246
|
+
getSessionId() {
|
|
247
|
+
return this.sessionId;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Check if reporting is active
|
|
251
|
+
*/
|
|
252
|
+
isActive() {
|
|
253
|
+
return this.intervalId !== null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
exports.TelemetryReporter = TelemetryReporter;
|
|
258
|
+
//# sourceMappingURL=TelemetryReporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryReporter.js","sources":["../../../../src/core/TelemetryReporter.ts"],"sourcesContent":["import type { TelemetryPayload, PlaybackQuality, ContentType } from \"../types\";\n\n/**\n * Generate a unique session ID\n */\nfunction generateSessionId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `${Date.now().toString(36)}-${hex}`;\n}\n\nexport interface TelemetryReporterConfig {\n /** Telemetry endpoint URL */\n endpoint: string;\n /** Auth token for endpoint */\n authToken?: string;\n /** Report interval in ms (default: 5000) */\n interval?: number;\n /** Batch size before flush (default: 1) */\n batchSize?: number;\n /** Content ID being played */\n contentId: string;\n /** Content type */\n contentType: ContentType;\n /** Player type name */\n playerType: string;\n /** Protocol being used */\n protocol: string;\n}\n\n/**\n * TelemetryReporter - Sends playback metrics to server\n *\n * Features:\n * - Batched reporting at configurable interval\n * - Retry with exponential backoff on failure\n * - Uses navigator.sendBeacon() for reliable page unload reporting\n * - Tracks errors during playback\n */\nexport class TelemetryReporter {\n private config: Required<TelemetryReporterConfig>;\n private sessionId: string;\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private pendingPayloads: TelemetryPayload[] = [];\n private errors: Array<{ code: string; message: string; timestamp: number }> = [];\n private stallCount = 0;\n private totalStallMs = 0;\n private lastStallStart = 0;\n private videoElement: HTMLVideoElement | null = null;\n private qualityGetter: (() => PlaybackQuality | null) | null = null;\n private listeners: Array<() => void> = [];\n\n constructor(config: TelemetryReporterConfig) {\n this.config = {\n endpoint: config.endpoint,\n authToken: config.authToken ?? \"\",\n interval: config.interval ?? 5000,\n batchSize: config.batchSize ?? 1,\n contentId: config.contentId,\n contentType: config.contentType,\n playerType: config.playerType,\n protocol: config.protocol,\n };\n this.sessionId = generateSessionId();\n }\n\n /**\n * Start telemetry reporting\n */\n start(videoElement: HTMLVideoElement, qualityGetter?: () => PlaybackQuality | null): void {\n this.stop();\n\n this.videoElement = videoElement;\n this.qualityGetter = qualityGetter ?? null;\n this.stallCount = 0;\n this.totalStallMs = 0;\n this.errors = [];\n\n // Track stalls\n const onWaiting = () => {\n this.stallCount++;\n this.lastStallStart = performance.now();\n };\n\n const onPlaying = () => {\n if (this.lastStallStart > 0) {\n this.totalStallMs += performance.now() - this.lastStallStart;\n this.lastStallStart = 0;\n }\n };\n\n const onError = () => {\n const error = videoElement.error;\n if (error) {\n this.errors.push({\n code: String(error.code),\n message: error.message || \"Unknown error\",\n timestamp: Date.now(),\n });\n }\n };\n\n videoElement.addEventListener(\"waiting\", onWaiting);\n videoElement.addEventListener(\"playing\", onPlaying);\n videoElement.addEventListener(\"error\", onError);\n\n this.listeners = [\n () => videoElement.removeEventListener(\"waiting\", onWaiting),\n () => videoElement.removeEventListener(\"playing\", onPlaying),\n () => videoElement.removeEventListener(\"error\", onError),\n ];\n\n // Setup unload handler for reliable final report\n const onUnload = () => this.flushSync();\n window.addEventListener(\"beforeunload\", onUnload);\n window.addEventListener(\"pagehide\", onUnload);\n this.listeners.push(\n () => window.removeEventListener(\"beforeunload\", onUnload),\n () => window.removeEventListener(\"pagehide\", onUnload)\n );\n\n // Start reporting interval\n this.intervalId = setInterval(() => this.report(), this.config.interval);\n\n // Take initial report\n this.report();\n }\n\n /**\n * Stop telemetry reporting\n */\n stop(): void {\n // Final report before stopping\n this.flushSync();\n\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n\n this.listeners.forEach((cleanup) => cleanup());\n this.listeners = [];\n\n this.videoElement = null;\n this.qualityGetter = null;\n }\n\n /**\n * Record a custom error\n */\n recordError(code: string, message: string): void {\n this.errors.push({\n code,\n message,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Generate telemetry payload\n */\n private generatePayload(): TelemetryPayload | null {\n const video = this.videoElement;\n if (!video) return null;\n\n // Get quality metrics if available\n const quality = this.qualityGetter?.() ?? null;\n\n // Get frame stats if available\n let framesDecoded = 0;\n let framesDropped = 0;\n\n if (\"getVideoPlaybackQuality\" in video) {\n const stats = video.getVideoPlaybackQuality();\n framesDecoded = stats.totalVideoFrames;\n framesDropped = stats.droppedVideoFrames;\n }\n\n // Calculate buffered seconds\n let bufferedSeconds = 0;\n if (video.buffered.length > 0) {\n for (let i = 0; i < video.buffered.length; i++) {\n if (\n video.buffered.start(i) <= video.currentTime &&\n video.buffered.end(i) > video.currentTime\n ) {\n bufferedSeconds = video.buffered.end(i) - video.currentTime;\n break;\n }\n }\n }\n\n return {\n timestamp: Date.now(),\n sessionId: this.sessionId,\n contentId: this.config.contentId,\n contentType: this.config.contentType,\n metrics: {\n currentTime: video.currentTime,\n duration: isFinite(video.duration) ? video.duration : -1,\n bufferedSeconds,\n stallCount: this.stallCount,\n totalStallMs: this.totalStallMs,\n bitrate: quality?.bitrate ?? 0,\n qualityScore: quality?.score ?? 100,\n framesDecoded,\n framesDropped,\n playerType: this.config.playerType,\n protocol: this.config.protocol,\n resolution:\n video.videoWidth > 0\n ? {\n width: video.videoWidth,\n height: video.videoHeight,\n }\n : undefined,\n },\n errors: this.errors.length > 0 ? [...this.errors] : undefined,\n };\n }\n\n /**\n * Send telemetry report\n */\n private async report(): Promise<void> {\n const payload = this.generatePayload();\n if (!payload) return;\n\n this.pendingPayloads.push(payload);\n\n // Flush if batch size reached\n if (this.pendingPayloads.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Flush pending payloads (async)\n */\n private async flush(): Promise<void> {\n if (this.pendingPayloads.length === 0) return;\n\n const payloads = [...this.pendingPayloads];\n this.pendingPayloads = [];\n\n try {\n const headers: HeadersInit = {\n \"Content-Type\": \"application/json\",\n };\n\n if (this.config.authToken) {\n headers[\"Authorization\"] = `Bearer ${this.config.authToken}`;\n }\n\n const response = await fetch(this.config.endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify(payloads.length === 1 ? payloads[0] : payloads),\n });\n\n if (!response.ok) {\n console.warn(\"[TelemetryReporter] Report failed:\", response.status);\n // Re-queue failed payloads (up to a limit)\n if (this.pendingPayloads.length < 10) {\n this.pendingPayloads.unshift(...payloads);\n }\n } else {\n // Clear reported errors\n this.errors = [];\n }\n } catch (error) {\n console.warn(\"[TelemetryReporter] Report error:\", error);\n // Re-queue failed payloads\n if (this.pendingPayloads.length < 10) {\n this.pendingPayloads.unshift(...payloads);\n }\n }\n }\n\n /**\n * Flush synchronously using sendBeacon (for page unload)\n */\n private flushSync(): void {\n const payload = this.generatePayload();\n if (!payload) return;\n\n const payloads = [...this.pendingPayloads, payload];\n this.pendingPayloads = [];\n\n try {\n const data = JSON.stringify(payloads.length === 1 ? payloads[0] : payloads);\n navigator.sendBeacon(this.config.endpoint, new Blob([data], { type: \"application/json\" }));\n } catch (error) {\n console.warn(\"[TelemetryReporter] Beacon failed:\", error);\n }\n }\n\n /**\n * Get session ID\n */\n getSessionId(): string {\n return this.sessionId;\n }\n\n /**\n * Check if reporting is active\n */\n isActive(): boolean {\n return this.intervalId !== null;\n }\n}\n\nexport default TelemetryReporter;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TimeFormat.ts
|
|
5
|
+
*
|
|
6
|
+
* Time formatting utilities for player controls.
|
|
7
|
+
* Used by React, Svelte, and Vanilla wrappers.
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Pure Functions
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Format seconds as MM:SS or HH:MM:SS.
|
|
14
|
+
*
|
|
15
|
+
* @param seconds - Time in seconds
|
|
16
|
+
* @returns Formatted time string, or "LIVE" for invalid input
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* formatTime(65) // "01:05"
|
|
20
|
+
* formatTime(3665) // "1:01:05"
|
|
21
|
+
* formatTime(-1) // "LIVE"
|
|
22
|
+
* formatTime(NaN) // "LIVE"
|
|
23
|
+
*/
|
|
24
|
+
function formatTime(seconds) {
|
|
25
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
26
|
+
return "LIVE";
|
|
27
|
+
}
|
|
28
|
+
const total = Math.floor(seconds);
|
|
29
|
+
const hours = Math.floor(total / 3600);
|
|
30
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
31
|
+
const secs = total % 60;
|
|
32
|
+
if (hours > 0) {
|
|
33
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format a Date as wall-clock time (HH:MM:SS).
|
|
39
|
+
*
|
|
40
|
+
* @param date - Date object
|
|
41
|
+
* @returns Formatted time string in HH:MM:SS format
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* formatClockTime(new Date('2024-01-15T14:30:45')) // "14:30:45"
|
|
45
|
+
*/
|
|
46
|
+
function formatClockTime(date) {
|
|
47
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
48
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
49
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
50
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format time display for player controls.
|
|
54
|
+
*
|
|
55
|
+
* For live streams:
|
|
56
|
+
* - With unixoffset: Shows actual wall-clock time (HH:MM:SS)
|
|
57
|
+
* - With seekable window: Shows time behind live (-MM:SS) or "LIVE"
|
|
58
|
+
* - Fallback: Shows elapsed time
|
|
59
|
+
*
|
|
60
|
+
* For VOD:
|
|
61
|
+
* - Shows "current / duration" (MM:SS / MM:SS)
|
|
62
|
+
*
|
|
63
|
+
* @param params - Display parameters
|
|
64
|
+
* @returns Formatted time display string
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Live with unixoffset
|
|
68
|
+
* formatTimeDisplay({ isLive: true, currentTime: 60, unixoffset: 1705330245000, ... })
|
|
69
|
+
* // "14:30:45"
|
|
70
|
+
*
|
|
71
|
+
* // Live behind
|
|
72
|
+
* formatTimeDisplay({ isLive: true, currentTime: 50, liveEdge: 60, ... })
|
|
73
|
+
* // "-00:10"
|
|
74
|
+
*
|
|
75
|
+
* // VOD
|
|
76
|
+
* formatTimeDisplay({ isLive: false, currentTime: 65, duration: 300, ... })
|
|
77
|
+
* // "01:05 / 05:00"
|
|
78
|
+
*/
|
|
79
|
+
function formatTimeDisplay(params) {
|
|
80
|
+
const { isLive, currentTime, duration, liveEdge, seekableStart, unixoffset } = params;
|
|
81
|
+
if (isLive) {
|
|
82
|
+
// For live: show actual wall-clock time using unixoffset
|
|
83
|
+
if (unixoffset && unixoffset > 0) {
|
|
84
|
+
// unixoffset is Unix timestamp in ms at timestamp 0 of the stream
|
|
85
|
+
// currentTime is playback position in seconds
|
|
86
|
+
const actualTimeMs = unixoffset + currentTime * 1000;
|
|
87
|
+
const actualDate = new Date(actualTimeMs);
|
|
88
|
+
return formatClockTime(actualDate);
|
|
89
|
+
}
|
|
90
|
+
// Fallback: show relative time if no unixoffset
|
|
91
|
+
const seekableWindow = liveEdge - seekableStart;
|
|
92
|
+
if (seekableWindow > 0) {
|
|
93
|
+
const behindSeconds = liveEdge - currentTime;
|
|
94
|
+
if (behindSeconds < 1) {
|
|
95
|
+
return "LIVE";
|
|
96
|
+
}
|
|
97
|
+
return `-${formatTime(Math.abs(behindSeconds))}`;
|
|
98
|
+
}
|
|
99
|
+
// No DVR window: show LIVE instead of a misleading timestamp
|
|
100
|
+
return "LIVE";
|
|
101
|
+
}
|
|
102
|
+
// VOD: show current / total
|
|
103
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
104
|
+
return `${formatTime(currentTime)} / ${formatTime(duration)}`;
|
|
105
|
+
}
|
|
106
|
+
return formatTime(currentTime);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format time for seek bar tooltip.
|
|
110
|
+
* For live streams, can show time relative to live edge.
|
|
111
|
+
*
|
|
112
|
+
* @param time - Time position in seconds
|
|
113
|
+
* @param isLive - Whether stream is live
|
|
114
|
+
* @param liveEdge - Live edge position (for relative display)
|
|
115
|
+
* @returns Formatted tooltip time
|
|
116
|
+
*/
|
|
117
|
+
function formatTooltipTime(time, isLive, liveEdge) {
|
|
118
|
+
if (isLive && liveEdge !== undefined && Number.isFinite(liveEdge)) {
|
|
119
|
+
const behindSeconds = liveEdge - time;
|
|
120
|
+
if (behindSeconds < 1) {
|
|
121
|
+
return "LIVE";
|
|
122
|
+
}
|
|
123
|
+
return `-${formatTime(Math.abs(behindSeconds))}`;
|
|
124
|
+
}
|
|
125
|
+
return formatTime(time);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Format duration for display (e.g., in stats panel).
|
|
129
|
+
* Handles edge cases like infinite duration for live streams.
|
|
130
|
+
*
|
|
131
|
+
* @param duration - Duration in seconds
|
|
132
|
+
* @param isLive - Whether content is live
|
|
133
|
+
* @returns Formatted duration string
|
|
134
|
+
*/
|
|
135
|
+
function formatDuration(duration, isLive) {
|
|
136
|
+
if (isLive || !Number.isFinite(duration)) {
|
|
137
|
+
return "LIVE";
|
|
138
|
+
}
|
|
139
|
+
return formatTime(duration);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse time string (HH:MM:SS or MM:SS) to seconds.
|
|
143
|
+
*
|
|
144
|
+
* @param timeStr - Time string to parse
|
|
145
|
+
* @returns Time in seconds, or NaN if invalid
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* parseTime("01:30") // 90
|
|
149
|
+
* parseTime("1:30:45") // 5445
|
|
150
|
+
* parseTime("invalid") // NaN
|
|
151
|
+
*/
|
|
152
|
+
function parseTime(timeStr) {
|
|
153
|
+
const parts = timeStr.split(":").map(Number);
|
|
154
|
+
if (parts.some(isNaN)) {
|
|
155
|
+
return NaN;
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 2) {
|
|
158
|
+
// MM:SS
|
|
159
|
+
const [minutes, seconds] = parts;
|
|
160
|
+
return minutes * 60 + seconds;
|
|
161
|
+
}
|
|
162
|
+
if (parts.length === 3) {
|
|
163
|
+
// HH:MM:SS
|
|
164
|
+
const [hours, minutes, seconds] = parts;
|
|
165
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
166
|
+
}
|
|
167
|
+
return NaN;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
exports.formatClockTime = formatClockTime;
|
|
171
|
+
exports.formatDuration = formatDuration;
|
|
172
|
+
exports.formatTime = formatTime;
|
|
173
|
+
exports.formatTimeDisplay = formatTimeDisplay;
|
|
174
|
+
exports.formatTooltipTime = formatTooltipTime;
|
|
175
|
+
exports.parseTime = parseTime;
|
|
176
|
+
//# sourceMappingURL=TimeFormat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeFormat.js","sources":["../../../../src/core/TimeFormat.ts"],"sourcesContent":["/**\n * TimeFormat.ts\n *\n * Time formatting utilities for player controls.\n * Used by React, Svelte, and Vanilla wrappers.\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TimeDisplayParams {\n isLive: boolean;\n currentTime: number;\n duration: number;\n liveEdge: number;\n seekableStart: number;\n /** Unix timestamp (ms) at stream time 0 - for wall-clock display */\n unixoffset?: number;\n}\n\n// ============================================================================\n// Pure Functions\n// ============================================================================\n\n/**\n * Format seconds as MM:SS or HH:MM:SS.\n *\n * @param seconds - Time in seconds\n * @returns Formatted time string, or \"LIVE\" for invalid input\n *\n * @example\n * formatTime(65) // \"01:05\"\n * formatTime(3665) // \"1:01:05\"\n * formatTime(-1) // \"LIVE\"\n * formatTime(NaN) // \"LIVE\"\n */\nexport function formatTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) {\n return \"LIVE\";\n }\n\n const total = Math.floor(seconds);\n const hours = Math.floor(total / 3600);\n const minutes = Math.floor((total % 3600) / 60);\n const secs = total % 60;\n\n if (hours > 0) {\n return `${hours}:${String(minutes).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n }\n\n return `${String(minutes).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n}\n\n/**\n * Format a Date as wall-clock time (HH:MM:SS).\n *\n * @param date - Date object\n * @returns Formatted time string in HH:MM:SS format\n *\n * @example\n * formatClockTime(new Date('2024-01-15T14:30:45')) // \"14:30:45\"\n */\nexport function formatClockTime(date: Date): string {\n const hours = String(date.getHours()).padStart(2, \"0\");\n const minutes = String(date.getMinutes()).padStart(2, \"0\");\n const seconds = String(date.getSeconds()).padStart(2, \"0\");\n return `${hours}:${minutes}:${seconds}`;\n}\n\n/**\n * Format time display for player controls.\n *\n * For live streams:\n * - With unixoffset: Shows actual wall-clock time (HH:MM:SS)\n * - With seekable window: Shows time behind live (-MM:SS) or \"LIVE\"\n * - Fallback: Shows elapsed time\n *\n * For VOD:\n * - Shows \"current / duration\" (MM:SS / MM:SS)\n *\n * @param params - Display parameters\n * @returns Formatted time display string\n *\n * @example\n * // Live with unixoffset\n * formatTimeDisplay({ isLive: true, currentTime: 60, unixoffset: 1705330245000, ... })\n * // \"14:30:45\"\n *\n * // Live behind\n * formatTimeDisplay({ isLive: true, currentTime: 50, liveEdge: 60, ... })\n * // \"-00:10\"\n *\n * // VOD\n * formatTimeDisplay({ isLive: false, currentTime: 65, duration: 300, ... })\n * // \"01:05 / 05:00\"\n */\nexport function formatTimeDisplay(params: TimeDisplayParams): string {\n const { isLive, currentTime, duration, liveEdge, seekableStart, unixoffset } = params;\n\n if (isLive) {\n // For live: show actual wall-clock time using unixoffset\n if (unixoffset && unixoffset > 0) {\n // unixoffset is Unix timestamp in ms at timestamp 0 of the stream\n // currentTime is playback position in seconds\n const actualTimeMs = unixoffset + currentTime * 1000;\n const actualDate = new Date(actualTimeMs);\n return formatClockTime(actualDate);\n }\n\n // Fallback: show relative time if no unixoffset\n const seekableWindow = liveEdge - seekableStart;\n if (seekableWindow > 0) {\n const behindSeconds = liveEdge - currentTime;\n if (behindSeconds < 1) {\n return \"LIVE\";\n }\n return `-${formatTime(Math.abs(behindSeconds))}`;\n }\n\n // No DVR window: show LIVE instead of a misleading timestamp\n return \"LIVE\";\n }\n\n // VOD: show current / total\n if (Number.isFinite(duration) && duration > 0) {\n return `${formatTime(currentTime)} / ${formatTime(duration)}`;\n }\n\n return formatTime(currentTime);\n}\n\n/**\n * Format time for seek bar tooltip.\n * For live streams, can show time relative to live edge.\n *\n * @param time - Time position in seconds\n * @param isLive - Whether stream is live\n * @param liveEdge - Live edge position (for relative display)\n * @returns Formatted tooltip time\n */\nexport function formatTooltipTime(time: number, isLive: boolean, liveEdge?: number): string {\n if (isLive && liveEdge !== undefined && Number.isFinite(liveEdge)) {\n const behindSeconds = liveEdge - time;\n if (behindSeconds < 1) {\n return \"LIVE\";\n }\n return `-${formatTime(Math.abs(behindSeconds))}`;\n }\n\n return formatTime(time);\n}\n\n/**\n * Format duration for display (e.g., in stats panel).\n * Handles edge cases like infinite duration for live streams.\n *\n * @param duration - Duration in seconds\n * @param isLive - Whether content is live\n * @returns Formatted duration string\n */\nexport function formatDuration(duration: number, isLive?: boolean): string {\n if (isLive || !Number.isFinite(duration)) {\n return \"LIVE\";\n }\n\n return formatTime(duration);\n}\n\n/**\n * Parse time string (HH:MM:SS or MM:SS) to seconds.\n *\n * @param timeStr - Time string to parse\n * @returns Time in seconds, or NaN if invalid\n *\n * @example\n * parseTime(\"01:30\") // 90\n * parseTime(\"1:30:45\") // 5445\n * parseTime(\"invalid\") // NaN\n */\nexport function parseTime(timeStr: string): number {\n const parts = timeStr.split(\":\").map(Number);\n\n if (parts.some(isNaN)) {\n return NaN;\n }\n\n if (parts.length === 2) {\n // MM:SS\n const [minutes, seconds] = parts;\n return minutes * 60 + seconds;\n }\n\n if (parts.length === 3) {\n // HH:MM:SS\n const [hours, minutes, seconds] = parts;\n return hours * 3600 + minutes * 60 + seconds;\n }\n\n return NaN;\n}\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAgBH;AACA;AACA;AAEA;;;;;;;;;;;AAWG;AACG,SAAU,UAAU,CAAC,OAAe,EAAA;AACxC,IAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE;AAC5C,QAAA,OAAO,MAAM;IACf;IAEA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;AACtC,IAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;AAC/C,IAAA,MAAM,IAAI,GAAG,KAAK,GAAG,EAAE;AAEvB,IAAA,IAAI,KAAK,GAAG,CAAC,EAAE;QACb,OAAO,CAAA,EAAG,KAAK,CAAA,CAAA,EAAI,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA,CAAA,EAAI,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA,CAAE;IACxF;IAEA,OAAO,CAAA,EAAG,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA,CAAE;AAC/E;AAEA;;;;;;;;AAQG;AACG,SAAU,eAAe,CAAC,IAAU,EAAA;AACxC,IAAA,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;AACtD,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;AAC1D,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;AAC1D,IAAA,OAAO,GAAG,KAAK,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,EAAI,OAAO,EAAE;AACzC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;AACG,SAAU,iBAAiB,CAAC,MAAyB,EAAA;AACzD,IAAA,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,MAAM;IAErF,IAAI,MAAM,EAAE;;AAEV,QAAA,IAAI,UAAU,IAAI,UAAU,GAAG,CAAC,EAAE;;;AAGhC,YAAA,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,IAAI;AACpD,YAAA,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC;AACzC,YAAA,OAAO,eAAe,CAAC,UAAU,CAAC;QACpC;;AAGA,QAAA,MAAM,cAAc,GAAG,QAAQ,GAAG,aAAa;AAC/C,QAAA,IAAI,cAAc,GAAG,CAAC,EAAE;AACtB,YAAA,MAAM,aAAa,GAAG,QAAQ,GAAG,WAAW;AAC5C,YAAA,IAAI,aAAa,GAAG,CAAC,EAAE;AACrB,gBAAA,OAAO,MAAM;YACf;YACA,OAAO,CAAA,CAAA,EAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAA,CAAE;QAClD;;AAGA,QAAA,OAAO,MAAM;IACf;;IAGA,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE;QAC7C,OAAO,CAAA,EAAG,UAAU,CAAC,WAAW,CAAC,CAAA,GAAA,EAAM,UAAU,CAAC,QAAQ,CAAC,CAAA,CAAE;IAC/D;AAEA,IAAA,OAAO,UAAU,CAAC,WAAW,CAAC;AAChC;AAEA;;;;;;;;AAQG;SACa,iBAAiB,CAAC,IAAY,EAAE,MAAe,EAAE,QAAiB,EAAA;AAChF,IAAA,IAAI,MAAM,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;AACjE,QAAA,MAAM,aAAa,GAAG,QAAQ,GAAG,IAAI;AACrC,QAAA,IAAI,aAAa,GAAG,CAAC,EAAE;AACrB,YAAA,OAAO,MAAM;QACf;QACA,OAAO,CAAA,CAAA,EAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAA,CAAE;IAClD;AAEA,IAAA,OAAO,UAAU,CAAC,IAAI,CAAC;AACzB;AAEA;;;;;;;AAOG;AACG,SAAU,cAAc,CAAC,QAAgB,EAAE,MAAgB,EAAA;IAC/D,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;AACxC,QAAA,OAAO,MAAM;IACf;AAEA,IAAA,OAAO,UAAU,CAAC,QAAQ,CAAC;AAC7B;AAEA;;;;;;;;;;AAUG;AACG,SAAU,SAAS,CAAC,OAAe,EAAA;AACvC,IAAA,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;AAE5C,IAAA,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AACrB,QAAA,OAAO,GAAG;IACZ;AAEA,IAAA,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;;AAEtB,QAAA,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK;AAChC,QAAA,OAAO,OAAO,GAAG,EAAE,GAAG,OAAO;IAC/B;AAEA,IAAA,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;;QAEtB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK;QACvC,OAAO,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO;IAC9C;AAEA,IAAA,OAAO,GAAG;AACZ;;;;;;;;;"}
|