@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.0
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/EF_FRAMEGEN.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +12 -2
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +118 -56
- package/dist/elements/EFThumbnailStrip.js +522 -358
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +223 -27
- package/dist/elements/EFTimegroup.js +850 -147
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +152 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +11 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +182 -4
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +840 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +68 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BufferUtils.js","names":["segments: number[]"],"sources":["../../../../src/elements/EFMedia/shared/BufferUtils.ts"],"sourcesContent":["import type {\n AudioRendition,\n VideoRendition,\n} from \"../../../transcoding/types\";\n\n/**\n * State interface for media buffering - orchestration only, no data storage\n */\nexport interface MediaBufferState {\n currentSeekTimeMs: number;\n requestedSegments: Set<number>; // Segments we've requested for buffering\n activeRequests: Set<number>; // Segments currently being fetched\n requestQueue: number[]; // Segments queued to be requested\n}\n\n/**\n * Configuration interface for media buffering - generic for both audio and video\n */\nexport interface MediaBufferConfig {\n bufferDurationMs: number;\n maxParallelFetches: number;\n enableBuffering: boolean;\n enableContinuousBuffering?: boolean;\n bufferThresholdMs?: number; // Timeline-aware buffering threshold (default: 30000ms)\n}\n\n/**\n * Dependencies interface for media buffering - integrates with BaseMediaEngine\n */\nexport interface MediaBufferDependencies<\n T extends AudioRendition | VideoRendition,\n> {\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>;\n prefetchSegment: (segmentId: number, rendition: T) => Promise<void>; // Just trigger prefetch, don't return data\n isSegmentCached: (segmentId: number, rendition: T) => boolean; // Check BaseMediaEngine cache\n getRendition: () => Promise<T | undefined>;\n logError: (message: string, error: any) => void;\n}\n\n/**\n * Compute segment range for a time window\n * Pure function - determines which segments are needed for a time range\n */\nexport const computeSegmentRange = <T extends AudioRendition | VideoRendition>(\n startTimeMs: number,\n endTimeMs: number,\n rendition: T,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): number[] => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(endTimeMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = computeSegmentId(i * segmentDurationMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Async version of computeSegmentRange for when computeSegmentId is async\n */\nexport const computeSegmentRangeAsync = async <\n T extends AudioRendition | VideoRendition,\n>(\n startTimeMs: number,\n endTimeMs: number,\n durationMs: number,\n rendition: T,\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>,\n): Promise<number[]> => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(\n Math.min(endTimeMs, durationMs) / segmentDurationMs,\n );\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const timeMs = i * segmentDurationMs;\n if (timeMs < durationMs) {\n const segmentId = await computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Compute buffer queue based on desired segments and what we've already requested\n * Pure function - determines what new segments should be prefetched\n */\nexport const computeBufferQueue = (\n desiredSegments: number[],\n requestedSegments: Set<number>,\n): number[] => {\n return desiredSegments.filter(\n (segmentId) => !requestedSegments.has(segmentId),\n );\n};\n\n/**\n * Handle seek time change and recompute buffer queue\n * Pure function - computes new queue when seek time changes\n */\nexport const handleSeekTimeChange = <T extends AudioRendition | VideoRendition>(\n newSeekTimeMs: number,\n bufferDurationMs: number,\n rendition: T,\n currentState: MediaBufferState,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): { newQueue: number[]; overlappingRequests: number[] } => {\n const endTimeMs = newSeekTimeMs + bufferDurationMs;\n const desiredSegments = computeSegmentRange(\n newSeekTimeMs,\n endTimeMs,\n rendition,\n computeSegmentId,\n );\n\n // Find segments that are already being requested\n const overlappingRequests = desiredSegments.filter((segmentId) =>\n currentState.requestedSegments.has(segmentId),\n );\n\n const newQueue = computeBufferQueue(\n desiredSegments,\n currentState.requestedSegments,\n );\n\n return { newQueue, overlappingRequests };\n};\n\n/**\n * Check if a segment has been requested for buffering\n * Pure function for checking buffer orchestration state\n */\nexport const isSegmentRequested = (\n segmentId: number,\n bufferState: MediaBufferState | undefined,\n): boolean => {\n return bufferState?.requestedSegments.has(segmentId) ?? false;\n};\n\n/**\n * Get requested segments from a list of segment IDs\n * Pure function that returns which segments have been requested for buffering\n */\nexport const getRequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): Set<number> => {\n if (!bufferState) {\n return new Set();\n }\n return new Set(\n segmentIds.filter((id) => bufferState.requestedSegments.has(id)),\n );\n};\n\n/**\n * Get unrequested segments from a list of segment IDs\n * Pure function that returns which segments haven't been requested yet\n */\nexport const getUnrequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): number[] => {\n if (!bufferState) {\n return segmentIds;\n }\n return segmentIds.filter((id) => !bufferState.requestedSegments.has(id));\n};\n\n/**\n * Calculate distance from element to playhead position\n * Returns 0 if element is currently active, otherwise returns distance in milliseconds\n */\nexport const calculatePlayheadDistance = (\n element: { startTimeMs: number; endTimeMs: number },\n playheadMs: number,\n): number => {\n // Element hasn't started yet\n if (playheadMs < element.startTimeMs) {\n return element.startTimeMs - playheadMs;\n }\n // Element already finished\n if (playheadMs > element.endTimeMs) {\n return playheadMs - element.endTimeMs;\n }\n // Element is currently active\n return 0;\n};\n\n/**\n * Core media buffering orchestration logic - prefetch only, no data storage\n * Integrates with BaseMediaEngine's existing caching and request deduplication\n */\nexport const manageMediaBuffer = async <\n T extends AudioRendition | VideoRendition,\n>(\n seekTimeMs: number,\n config: MediaBufferConfig,\n currentState: MediaBufferState,\n durationMs: number,\n signal: AbortSignal,\n deps: MediaBufferDependencies<T>,\n timelineContext?: {\n elementStartMs: number;\n elementEndMs: number;\n playheadMs: number;\n },\n): Promise<MediaBufferState> => {\n if (!config.enableBuffering) {\n return currentState;\n }\n\n // Timeline-aware buffering: skip if element is too far from playhead\n if (timelineContext && config.bufferThresholdMs !== undefined) {\n const distance = calculatePlayheadDistance(\n {\n startTimeMs: timelineContext.elementStartMs,\n endTimeMs: timelineContext.elementEndMs,\n },\n timelineContext.playheadMs,\n );\n\n if (distance > config.bufferThresholdMs) {\n // Element is too far from playhead, skip buffering\n return currentState;\n }\n }\n\n const rendition = await deps.getRendition();\n if (!rendition) {\n // Cannot buffer without a rendition\n return currentState;\n }\n const endTimeMs = seekTimeMs + config.bufferDurationMs;\n\n const desiredSegments = await computeSegmentRangeAsync(\n seekTimeMs,\n endTimeMs,\n durationMs,\n rendition,\n deps.computeSegmentId,\n );\n // Filter out segments already cached by BaseMediaEngine\n const uncachedSegments = desiredSegments.filter(\n (segmentId) => !deps.isSegmentCached(segmentId, rendition),\n );\n\n const newQueue = computeBufferQueue(\n uncachedSegments,\n currentState.requestedSegments,\n );\n\n // Shared state for concurrency control - prevents race conditions\n const newRequestedSegments = new Set(currentState.requestedSegments);\n const newActiveRequests = new Set(currentState.activeRequests);\n const remainingQueue = [...newQueue];\n\n // Thread-safe function to start next segment when slot becomes available\n const startNextSegment = (): void => {\n // Check if we have capacity and segments to fetch\n if (\n newActiveRequests.size >= config.maxParallelFetches ||\n remainingQueue.length === 0 ||\n signal.aborted\n ) {\n return;\n }\n\n const nextSegmentId = remainingQueue.shift();\n if (nextSegmentId === undefined) return;\n\n // Skip if already requested or now cached\n if (\n newRequestedSegments.has(nextSegmentId) ||\n deps.isSegmentCached(nextSegmentId, rendition)\n ) {\n startNextSegment(); // Try next segment immediately\n return;\n }\n\n newRequestedSegments.add(nextSegmentId);\n newActiveRequests.add(nextSegmentId);\n\n // Start the prefetch request\n deps\n .prefetchSegment(nextSegmentId, rendition)\n .then(() => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n // Start next segment if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n })\n .catch((error) => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);\n // Continue even after error if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n });\n };\n\n // Start initial batch of requests up to maxParallelFetches limit\n const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);\n for (let i = 0; i < initialBatchSize; i++) {\n startNextSegment();\n }\n\n const result = {\n currentSeekTimeMs: seekTimeMs,\n requestedSegments: newRequestedSegments,\n activeRequests: newActiveRequests,\n requestQueue: remainingQueue, // What's left in the queue\n };\n return result;\n};\n"],"mappings":";;;;AAwEA,MAAa,2BAA2B,OAGtC,aACA,WACA,YACA,WACA,qBAIsB;CACtB,MAAMA,WAAqB,EAAE;CAC7B,MAAM,oBAAqB,UAAkB,qBAAqB;CAGlE,MAAM,oBAAoB,KAAK,MAAM,cAAc,kBAAkB;CACrE,MAAM,kBAAkB,KAAK,MAC3B,KAAK,IAAI,WAAW,WAAW,GAAG,kBACnC;AAED,MAAK,IAAI,IAAI,mBAAmB,KAAK,iBAAiB,KAAK;EACzD,MAAM,SAAS,IAAI;AACnB,MAAI,SAAS,YAAY;GACvB,MAAM,YAAY,MAAM,iBAAiB,QAAQ,UAAU;AAC3D,OAAI,cAAc,OAChB,UAAS,KAAK,UAAU;;;AAK9B,QAAO,SAAS,QAAQ,IAAI,OAAO,QAAQ,IAAI,QAAQ,GAAG,KAAK,MAAM;;;;;;AAOvE,MAAa,sBACX,iBACA,sBACa;AACb,QAAO,gBAAgB,QACpB,cAAc,CAAC,kBAAkB,IAAI,UAAU,CACjD;;;;;;AAgFH,MAAa,6BACX,SACA,eACW;AAEX,KAAI,aAAa,QAAQ,YACvB,QAAO,QAAQ,cAAc;AAG/B,KAAI,aAAa,QAAQ,UACvB,QAAO,aAAa,QAAQ;AAG9B,QAAO;;;;;;AAOT,MAAa,oBAAoB,OAG/B,YACA,QACA,cACA,YACA,QACA,MACA,oBAK8B;AAC9B,KAAI,CAAC,OAAO,gBACV,QAAO;AAIT,KAAI,mBAAmB,OAAO,sBAAsB,QASlD;MARiB,0BACf;GACE,aAAa,gBAAgB;GAC7B,WAAW,gBAAgB;GAC5B,EACD,gBAAgB,WACjB,GAEc,OAAO,kBAEpB,QAAO;;CAIX,MAAM,YAAY,MAAM,KAAK,cAAc;AAC3C,KAAI,CAAC,UAEH,QAAO;CAgBT,MAAM,WAAW,oBAZO,MAAM,yBAC5B,YAHgB,aAAa,OAAO,kBAKpC,YACA,WACA,KAAK,iBACN,EAEwC,QACtC,cAAc,CAAC,KAAK,gBAAgB,WAAW,UAAU,CAC3D,EAIC,aAAa,kBACd;CAGD,MAAM,uBAAuB,IAAI,IAAI,aAAa,kBAAkB;CACpE,MAAM,oBAAoB,IAAI,IAAI,aAAa,eAAe;CAC9D,MAAM,iBAAiB,CAAC,GAAG,SAAS;CAGpC,MAAM,yBAA+B;AAEnC,MACE,kBAAkB,QAAQ,OAAO,sBACjC,eAAe,WAAW,KAC1B,OAAO,QAEP;EAGF,MAAM,gBAAgB,eAAe,OAAO;AAC5C,MAAI,kBAAkB,OAAW;AAGjC,MACE,qBAAqB,IAAI,cAAc,IACvC,KAAK,gBAAgB,eAAe,UAAU,EAC9C;AACA,qBAAkB;AAClB;;AAGF,uBAAqB,IAAI,cAAc;AACvC,oBAAkB,IAAI,cAAc;AAGpC,OACG,gBAAgB,eAAe,UAAU,CACzC,WAAW;AACV,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AAEvC,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB,CACD,OAAO,UAAU;AAChB,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AACvC,QAAK,SAAS,8BAA8B,iBAAiB,MAAM;AAEnE,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB;;CAIN,MAAM,mBAAmB,KAAK,IAAI,OAAO,oBAAoB,SAAS,OAAO;AAC7E,MAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,IACpC,mBAAkB;AASpB,QANe;EACb,mBAAmB;EACnB,mBAAmB;EACnB,gBAAgB;EAChB,cAAc;EACf"}
|
|
1
|
+
{"version":3,"file":"BufferUtils.js","names":["segments: number[]"],"sources":["../../../../src/elements/EFMedia/shared/BufferUtils.ts"],"sourcesContent":["import type {\n AudioRendition,\n VideoRendition,\n} from \"../../../transcoding/types\";\n\n/**\n * State interface for media buffering - orchestration only, no data storage\n */\nexport interface MediaBufferState {\n currentSeekTimeMs: number;\n requestedSegments: Set<number>; // Segments we've requested for buffering\n activeRequests: Set<number>; // Segments currently being fetched\n requestQueue: number[]; // Segments queued to be requested\n}\n\n/**\n * Configuration interface for media buffering - generic for both audio and video\n */\nexport interface MediaBufferConfig {\n bufferDurationMs: number;\n maxParallelFetches: number;\n enableBuffering: boolean;\n enableContinuousBuffering?: boolean;\n bufferThresholdMs?: number; // Timeline-aware buffering threshold (default: 30000ms)\n}\n\n/**\n * Dependencies interface for media buffering - integrates with BaseMediaEngine\n */\nexport interface MediaBufferDependencies<\n T extends AudioRendition | VideoRendition,\n> {\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>;\n prefetchSegment: (segmentId: number, rendition: T) => Promise<void>; // Just trigger prefetch, don't return data\n isSegmentCached: (segmentId: number, rendition: T) => boolean; // Check BaseMediaEngine cache\n getRendition: () => Promise<T | undefined>;\n logError: (message: string, error: any) => void;\n}\n\n/**\n * Compute segment range for a time window\n * Pure function - determines which segments are needed for a time range\n */\nexport const computeSegmentRange = <T extends AudioRendition | VideoRendition>(\n startTimeMs: number,\n endTimeMs: number,\n rendition: T,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): number[] => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(endTimeMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = computeSegmentId(i * segmentDurationMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Async version of computeSegmentRange for when computeSegmentId is async\n */\nexport const computeSegmentRangeAsync = async <\n T extends AudioRendition | VideoRendition,\n>(\n startTimeMs: number,\n endTimeMs: number,\n durationMs: number,\n rendition: T,\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>,\n): Promise<number[]> => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(\n Math.min(endTimeMs, durationMs) / segmentDurationMs,\n );\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const timeMs = i * segmentDurationMs;\n if (timeMs < durationMs) {\n const segmentId = await computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Compute buffer queue based on desired segments and what we've already requested\n * Pure function - determines what new segments should be prefetched\n */\nexport const computeBufferQueue = (\n desiredSegments: number[],\n requestedSegments: Set<number>,\n): number[] => {\n return desiredSegments.filter(\n (segmentId) => !requestedSegments.has(segmentId),\n );\n};\n\n/**\n * Handle seek time change and recompute buffer queue\n * Pure function - computes new queue when seek time changes\n */\nexport const handleSeekTimeChange = <T extends AudioRendition | VideoRendition>(\n newSeekTimeMs: number,\n bufferDurationMs: number,\n rendition: T,\n currentState: MediaBufferState,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): { newQueue: number[]; overlappingRequests: number[] } => {\n const endTimeMs = newSeekTimeMs + bufferDurationMs;\n const desiredSegments = computeSegmentRange(\n newSeekTimeMs,\n endTimeMs,\n rendition,\n computeSegmentId,\n );\n\n // Find segments that are already being requested\n const overlappingRequests = desiredSegments.filter((segmentId) =>\n currentState.requestedSegments.has(segmentId),\n );\n\n const newQueue = computeBufferQueue(\n desiredSegments,\n currentState.requestedSegments,\n );\n\n return { newQueue, overlappingRequests };\n};\n\n/**\n * Check if a segment has been requested for buffering\n * Pure function for checking buffer orchestration state\n */\nexport const isSegmentRequested = (\n segmentId: number,\n bufferState: MediaBufferState | undefined,\n): boolean => {\n return bufferState?.requestedSegments.has(segmentId) ?? false;\n};\n\n/**\n * Get requested segments from a list of segment IDs\n * Pure function that returns which segments have been requested for buffering\n */\nexport const getRequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): Set<number> => {\n if (!bufferState) {\n return new Set();\n }\n return new Set(\n segmentIds.filter((id) => bufferState.requestedSegments.has(id)),\n );\n};\n\n/**\n * Get unrequested segments from a list of segment IDs\n * Pure function that returns which segments haven't been requested yet\n */\nexport const getUnrequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): number[] => {\n if (!bufferState) {\n return segmentIds;\n }\n return segmentIds.filter((id) => !bufferState.requestedSegments.has(id));\n};\n\n/**\n * Calculate distance from element to playhead position\n * Returns 0 if element is currently active, otherwise returns distance in milliseconds\n */\nexport const calculatePlayheadDistance = (\n element: { startTimeMs: number; endTimeMs: number },\n playheadMs: number,\n): number => {\n // Element hasn't started yet\n if (playheadMs < element.startTimeMs) {\n return element.startTimeMs - playheadMs;\n }\n // Element already finished\n if (playheadMs > element.endTimeMs) {\n return playheadMs - element.endTimeMs;\n }\n // Element is currently active\n return 0;\n};\n\n/**\n * Core media buffering orchestration logic - prefetch only, no data storage\n * Integrates with BaseMediaEngine's existing caching and request deduplication\n */\nexport const manageMediaBuffer = async <\n T extends AudioRendition | VideoRendition,\n>(\n seekTimeMs: number,\n config: MediaBufferConfig,\n currentState: MediaBufferState,\n durationMs: number,\n signal: AbortSignal,\n deps: MediaBufferDependencies<T>,\n timelineContext?: {\n elementStartMs: number;\n elementEndMs: number;\n playheadMs: number;\n },\n): Promise<MediaBufferState> => {\n if (!config.enableBuffering) {\n return currentState;\n }\n\n // Timeline-aware buffering: skip if element is too far from playhead\n if (timelineContext && config.bufferThresholdMs !== undefined) {\n const distance = calculatePlayheadDistance(\n {\n startTimeMs: timelineContext.elementStartMs,\n endTimeMs: timelineContext.elementEndMs,\n },\n timelineContext.playheadMs,\n );\n\n if (distance > config.bufferThresholdMs) {\n // Element is too far from playhead, skip buffering\n return currentState;\n }\n }\n\n const rendition = await deps.getRendition();\n if (!rendition) {\n // Cannot buffer without a rendition\n return currentState;\n }\n const endTimeMs = seekTimeMs + config.bufferDurationMs;\n\n const desiredSegments = await computeSegmentRangeAsync(\n seekTimeMs,\n endTimeMs,\n durationMs,\n rendition,\n deps.computeSegmentId,\n );\n // Filter out segments already cached by BaseMediaEngine\n const uncachedSegments = desiredSegments.filter(\n (segmentId) => !deps.isSegmentCached(segmentId, rendition),\n );\n\n const newQueue = computeBufferQueue(\n uncachedSegments,\n currentState.requestedSegments,\n );\n\n // Shared state for concurrency control - prevents race conditions\n const newRequestedSegments = new Set(currentState.requestedSegments);\n const newActiveRequests = new Set(currentState.activeRequests);\n const remainingQueue = [...newQueue];\n\n // Thread-safe function to start next segment when slot becomes available\n const startNextSegment = (): void => {\n // Check if we have capacity and segments to fetch\n if (\n newActiveRequests.size >= config.maxParallelFetches ||\n remainingQueue.length === 0 ||\n signal.aborted\n ) {\n return;\n }\n\n const nextSegmentId = remainingQueue.shift();\n if (nextSegmentId === undefined) return;\n\n // Skip if already requested or now cached\n if (\n newRequestedSegments.has(nextSegmentId) ||\n deps.isSegmentCached(nextSegmentId, rendition)\n ) {\n startNextSegment(); // Try next segment immediately\n return;\n }\n\n newRequestedSegments.add(nextSegmentId);\n newActiveRequests.add(nextSegmentId);\n\n // Start the prefetch request\n deps\n .prefetchSegment(nextSegmentId, rendition)\n .then(() => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n // Start next segment if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n })\n .catch((error) => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n \n // Don't log AbortError - these are intentional request cancellations\n const isAbortError = \n error instanceof DOMException && error.name === \"AbortError\" ||\n (error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")\n ));\n \n if (!isAbortError) {\n deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);\n }\n \n // Continue even after error if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n });\n };\n\n // Start initial batch of requests up to maxParallelFetches limit\n const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);\n for (let i = 0; i < initialBatchSize; i++) {\n startNextSegment();\n }\n\n const result = {\n currentSeekTimeMs: seekTimeMs,\n requestedSegments: newRequestedSegments,\n activeRequests: newActiveRequests,\n requestQueue: remainingQueue, // What's left in the queue\n };\n return result;\n};\n"],"mappings":";;;;AAwEA,MAAa,2BAA2B,OAGtC,aACA,WACA,YACA,WACA,qBAIsB;CACtB,MAAMA,WAAqB,EAAE;CAC7B,MAAM,oBAAqB,UAAkB,qBAAqB;CAGlE,MAAM,oBAAoB,KAAK,MAAM,cAAc,kBAAkB;CACrE,MAAM,kBAAkB,KAAK,MAC3B,KAAK,IAAI,WAAW,WAAW,GAAG,kBACnC;AAED,MAAK,IAAI,IAAI,mBAAmB,KAAK,iBAAiB,KAAK;EACzD,MAAM,SAAS,IAAI;AACnB,MAAI,SAAS,YAAY;GACvB,MAAM,YAAY,MAAM,iBAAiB,QAAQ,UAAU;AAC3D,OAAI,cAAc,OAChB,UAAS,KAAK,UAAU;;;AAK9B,QAAO,SAAS,QAAQ,IAAI,OAAO,QAAQ,IAAI,QAAQ,GAAG,KAAK,MAAM;;;;;;AAOvE,MAAa,sBACX,iBACA,sBACa;AACb,QAAO,gBAAgB,QACpB,cAAc,CAAC,kBAAkB,IAAI,UAAU,CACjD;;;;;;AAgFH,MAAa,6BACX,SACA,eACW;AAEX,KAAI,aAAa,QAAQ,YACvB,QAAO,QAAQ,cAAc;AAG/B,KAAI,aAAa,QAAQ,UACvB,QAAO,aAAa,QAAQ;AAG9B,QAAO;;;;;;AAOT,MAAa,oBAAoB,OAG/B,YACA,QACA,cACA,YACA,QACA,MACA,oBAK8B;AAC9B,KAAI,CAAC,OAAO,gBACV,QAAO;AAIT,KAAI,mBAAmB,OAAO,sBAAsB,QASlD;MARiB,0BACf;GACE,aAAa,gBAAgB;GAC7B,WAAW,gBAAgB;GAC5B,EACD,gBAAgB,WACjB,GAEc,OAAO,kBAEpB,QAAO;;CAIX,MAAM,YAAY,MAAM,KAAK,cAAc;AAC3C,KAAI,CAAC,UAEH,QAAO;CAgBT,MAAM,WAAW,oBAZO,MAAM,yBAC5B,YAHgB,aAAa,OAAO,kBAKpC,YACA,WACA,KAAK,iBACN,EAEwC,QACtC,cAAc,CAAC,KAAK,gBAAgB,WAAW,UAAU,CAC3D,EAIC,aAAa,kBACd;CAGD,MAAM,uBAAuB,IAAI,IAAI,aAAa,kBAAkB;CACpE,MAAM,oBAAoB,IAAI,IAAI,aAAa,eAAe;CAC9D,MAAM,iBAAiB,CAAC,GAAG,SAAS;CAGpC,MAAM,yBAA+B;AAEnC,MACE,kBAAkB,QAAQ,OAAO,sBACjC,eAAe,WAAW,KAC1B,OAAO,QAEP;EAGF,MAAM,gBAAgB,eAAe,OAAO;AAC5C,MAAI,kBAAkB,OAAW;AAGjC,MACE,qBAAqB,IAAI,cAAc,IACvC,KAAK,gBAAgB,eAAe,UAAU,EAC9C;AACA,qBAAkB;AAClB;;AAGF,uBAAqB,IAAI,cAAc;AACvC,oBAAkB,IAAI,cAAc;AAGpC,OACG,gBAAgB,eAAe,UAAU,CACzC,WAAW;AACV,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AAEvC,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB,CACD,OAAO,UAAU;AAChB,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AAWvC,OAAI,EAPF,iBAAiB,gBAAgB,MAAM,SAAS,gBAC/C,iBAAiB,UAChB,MAAM,SAAS,gBACf,MAAM,QAAQ,SAAS,oBAAoB,IAC3C,MAAM,QAAQ,SAAS,6BAA6B,GAItD,MAAK,SAAS,8BAA8B,iBAAiB,MAAM;AAIrE,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB;;CAIN,MAAM,mBAAmB,KAAK,IAAI,OAAO,oBAAoB,SAAS,OAAO;AAC7E,MAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,IACpC,mBAAkB;AASpB,QANe;EACb,mBAAmB;EACnB,mBAAmB;EACnB,gBAAgB;EAChB,cAAc;EACf"}
|
|
@@ -13,7 +13,7 @@ var ThumbnailExtractor = class {
|
|
|
13
13
|
/**
|
|
14
14
|
* Extract thumbnails at multiple timestamps efficiently using segment batching
|
|
15
15
|
*/
|
|
16
|
-
async extractThumbnails(timestamps, rendition, durationMs) {
|
|
16
|
+
async extractThumbnails(timestamps, rendition, durationMs, signal) {
|
|
17
17
|
if (timestamps.length === 0) return [];
|
|
18
18
|
const validTimestamps = timestamps.filter((timeMs) => timeMs >= 0 && timeMs <= durationMs);
|
|
19
19
|
if (validTimestamps.length === 0) {
|
|
@@ -22,12 +22,16 @@ var ThumbnailExtractor = class {
|
|
|
22
22
|
}
|
|
23
23
|
const segmentGroups = this.groupTimestampsBySegment(validTimestamps, rendition);
|
|
24
24
|
const results = /* @__PURE__ */ new Map();
|
|
25
|
-
for (const [segmentId, segmentTimestamps] of segmentGroups)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
for (const [segmentId, segmentTimestamps] of segmentGroups) {
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
try {
|
|
28
|
+
const segmentResults = await this.extractSegmentThumbnails(segmentId, segmentTimestamps, rendition, signal);
|
|
29
|
+
for (const [timestamp, thumbnail] of segmentResults) results.set(timestamp, thumbnail);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
32
|
+
console.warn(`ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`, error);
|
|
33
|
+
for (const timestamp of segmentTimestamps) results.set(timestamp, null);
|
|
34
|
+
}
|
|
31
35
|
}
|
|
32
36
|
return timestamps.map((t) => {
|
|
33
37
|
if (t < 0 || t > durationMs) return null;
|
|
@@ -55,11 +59,16 @@ var ThumbnailExtractor = class {
|
|
|
55
59
|
/**
|
|
56
60
|
* Extract thumbnails for a specific segment using CanvasSink
|
|
57
61
|
*/
|
|
58
|
-
async extractSegmentThumbnails(segmentId, timestamps, rendition) {
|
|
62
|
+
async extractSegmentThumbnails(segmentId, timestamps, rendition, signal) {
|
|
59
63
|
const results = /* @__PURE__ */ new Map();
|
|
60
64
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
signal?.throwIfAborted();
|
|
66
|
+
if (!signal) {
|
|
67
|
+
for (const timestamp of timestamps) results.set(timestamp, null);
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
const [initSegment, mediaSegment] = await Promise.all([this.mediaEngine.fetchInitSegment(rendition, signal), this.mediaEngine.fetchMediaSegment(segmentId, rendition, signal)]);
|
|
71
|
+
signal?.throwIfAborted();
|
|
63
72
|
const segmentBlob = new Blob([initSegment, mediaSegment]);
|
|
64
73
|
let input = globalInputCache.get(rendition.src, segmentId, rendition.id);
|
|
65
74
|
if (!input) {
|
|
@@ -75,11 +84,12 @@ var ThumbnailExtractor = class {
|
|
|
75
84
|
return results;
|
|
76
85
|
}
|
|
77
86
|
const sink = new CanvasSink(videoTrack);
|
|
78
|
-
const
|
|
87
|
+
const sortedTimestamps = [...timestamps].sort((a, b) => a - b);
|
|
88
|
+
const relativeTimestamps = this.convertToSegmentRelativeTimestamps(sortedTimestamps, segmentId, rendition);
|
|
79
89
|
const timestampResults = [];
|
|
80
90
|
for await (const result of sink.canvasesAtTimestamps(relativeTimestamps)) timestampResults.push(result);
|
|
81
|
-
for (let i = 0; i <
|
|
82
|
-
const globalTimestamp =
|
|
91
|
+
for (let i = 0; i < sortedTimestamps.length; i++) {
|
|
92
|
+
const globalTimestamp = sortedTimestamps[i];
|
|
83
93
|
if (globalTimestamp === void 0) continue;
|
|
84
94
|
const result = timestampResults[i];
|
|
85
95
|
if (result?.canvas) {
|
|
@@ -92,7 +102,8 @@ var ThumbnailExtractor = class {
|
|
|
92
102
|
} else results.set(globalTimestamp, null);
|
|
93
103
|
}
|
|
94
104
|
} catch (error) {
|
|
95
|
-
|
|
105
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
106
|
+
console.warn(`ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`, error);
|
|
96
107
|
for (const timestamp of timestamps) results.set(timestamp, null);
|
|
97
108
|
}
|
|
98
109
|
return results;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: BaseMediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type {\n ThumbnailResult,\n VideoRendition,\n} from \"../../../transcoding/types/index.js\";\nimport type { BaseMediaEngine } from \"../BaseMediaEngine.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\n\n/**\n * Shared thumbnail extraction logic for all MediaEngine implementations\n * Eliminates code duplication and provides consistent behavior\n */\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: BaseMediaEngine) {}\n\n /**\n * Extract thumbnails at multiple timestamps efficiently using segment batching\n */\n async extractThumbnails(\n timestamps: number[],\n rendition: VideoRendition,\n durationMs: number,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n // Validate and filter timestamps within bounds\n const validTimestamps = timestamps.filter(\n (timeMs) => timeMs >= 0 && timeMs <= durationMs,\n );\n\n if (validTimestamps.length === 0) {\n console.warn(\n `ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`,\n );\n return timestamps.map(() => null);\n }\n\n // Group timestamps by segment for batch processing\n const segmentGroups = this.groupTimestampsBySegment(\n validTimestamps,\n rendition,\n );\n\n // Extract batched by segment using CanvasSink\n const results = new Map<number, ThumbnailResult | null>();\n\n for (const [segmentId, segmentTimestamps] of segmentGroups) {\n try {\n const segmentResults = await this.extractSegmentThumbnails(\n segmentId,\n segmentTimestamps,\n rendition,\n );\n\n for (const [timestamp, thumbnail] of segmentResults) {\n results.set(timestamp, thumbnail);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Mark all timestamps in this segment as failed\n for (const timestamp of segmentTimestamps) {\n results.set(timestamp, null);\n }\n }\n }\n\n // Return in original order, null for any that failed or were out of bounds\n return timestamps.map((t) => {\n // If timestamp was out of bounds, return null\n if (t < 0 || t > durationMs) {\n return null;\n }\n return results.get(t) || null;\n });\n }\n\n /**\n * Group timestamps by segment ID for efficient batch processing\n */\n private groupTimestampsBySegment(\n timestamps: number[],\n rendition: VideoRendition,\n ): Map<number, number[]> {\n const segmentGroups = new Map<number, number[]>();\n\n for (const timeMs of timestamps) {\n try {\n const segmentId = this.mediaEngine.computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n if (!segmentGroups.has(segmentId)) {\n segmentGroups.set(segmentId, []);\n }\n const segmentGroup = segmentGroups.get(segmentId) ?? [];\n if (!segmentGroup) {\n segmentGroups.set(segmentId, []);\n }\n segmentGroup.push(timeMs);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`,\n error,\n );\n }\n }\n\n return segmentGroups;\n }\n\n /**\n * Extract thumbnails for a specific segment using CanvasSink\n */\n private async extractSegmentThumbnails(\n segmentId: number,\n timestamps: number[],\n rendition: VideoRendition,\n ): Promise<Map<number, ThumbnailResult | null>> {\n const results = new Map<number, ThumbnailResult | null>();\n\n try {\n // Get segment data through existing media engine methods (uses caches)\n const abortController = new AbortController();\n const [initSegment, mediaSegment] = await Promise.all([\n this.mediaEngine.fetchInitSegment(rendition, abortController.signal),\n this.mediaEngine.fetchMediaSegment(segmentId, rendition),\n ]);\n\n // Create Input for this segment using global shared cache\n const segmentBlob = new Blob([initSegment, mediaSegment]);\n\n let input = globalInputCache.get(rendition.src, segmentId, rendition.id);\n if (!input) {\n input = new Input({\n formats: ALL_FORMATS,\n source: new BlobSource(segmentBlob),\n });\n globalInputCache.set(rendition.src, segmentId, input, rendition.id);\n }\n\n // Set up CanvasSink for batched extraction\n const videoTrack = await input.getPrimaryVideoTrack();\n if (!videoTrack) {\n // No video track - return nulls for all timestamps\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n\n const sink = new CanvasSink(videoTrack);\n\n // Convert global timestamps to segment-relative (in seconds for mediabunny)\n const relativeTimestamps = this.convertToSegmentRelativeTimestamps(\n timestamps,\n segmentId,\n rendition,\n );\n\n // Batch extract all thumbnails for this segment\n const timestampResults = [];\n for await (const result of sink.canvasesAtTimestamps(\n relativeTimestamps,\n )) {\n timestampResults.push(result);\n }\n\n // Map results back to original timestamps\n for (let i = 0; i < timestamps.length; i++) {\n const globalTimestamp = timestamps[i];\n if (globalTimestamp === undefined) {\n continue;\n }\n\n const result = timestampResults[i];\n\n if (result?.canvas) {\n const canvas = result.canvas;\n if (\n canvas instanceof HTMLCanvasElement ||\n canvas instanceof OffscreenCanvas\n ) {\n results.set(globalTimestamp, {\n timestamp: globalTimestamp,\n thumbnail: canvas,\n });\n } else {\n results.set(globalTimestamp, null);\n }\n } else {\n results.set(globalTimestamp, null);\n }\n }\n } catch (error) {\n console.error(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Return nulls for all timestamps on error\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n }\n\n return results;\n }\n\n /**\n * Convert global timestamps to segment-relative timestamps for mediabunny\n * This is where the main difference between JIT and Asset engines lies\n */\n private convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n return this.mediaEngine.convertToSegmentRelativeTimestamps(\n globalTimestamps,\n segmentId,\n rendition,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: BaseMediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type {\n ThumbnailResult,\n VideoRendition,\n} from \"../../../transcoding/types/index.js\";\nimport type { BaseMediaEngine } from \"../BaseMediaEngine.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\n\n/**\n * Shared thumbnail extraction logic for all MediaEngine implementations\n * Eliminates code duplication and provides consistent behavior\n */\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: BaseMediaEngine) {}\n\n /**\n * Extract thumbnails at multiple timestamps efficiently using segment batching\n */\n async extractThumbnails(\n timestamps: number[],\n rendition: VideoRendition,\n durationMs: number,\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n // Validate and filter timestamps within bounds\n const validTimestamps = timestamps.filter(\n (timeMs) => timeMs >= 0 && timeMs <= durationMs,\n );\n\n if (validTimestamps.length === 0) {\n console.warn(\n `ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`,\n );\n return timestamps.map(() => null);\n }\n\n // Group timestamps by segment for batch processing\n const segmentGroups = this.groupTimestampsBySegment(\n validTimestamps,\n rendition,\n );\n\n // Extract batched by segment using CanvasSink\n const results = new Map<number, ThumbnailResult | null>();\n\n for (const [segmentId, segmentTimestamps] of segmentGroups) {\n // Check abort before processing each segment\n signal?.throwIfAborted();\n \n try {\n const segmentResults = await this.extractSegmentThumbnails(\n segmentId,\n segmentTimestamps,\n rendition,\n signal,\n );\n\n for (const [timestamp, thumbnail] of segmentResults) {\n results.set(timestamp, thumbnail);\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Mark all timestamps in this segment as failed\n for (const timestamp of segmentTimestamps) {\n results.set(timestamp, null);\n }\n }\n }\n\n // Return in original order, null for any that failed or were out of bounds\n return timestamps.map((t) => {\n // If timestamp was out of bounds, return null\n if (t < 0 || t > durationMs) {\n return null;\n }\n return results.get(t) || null;\n });\n }\n\n /**\n * Group timestamps by segment ID for efficient batch processing\n */\n private groupTimestampsBySegment(\n timestamps: number[],\n rendition: VideoRendition,\n ): Map<number, number[]> {\n const segmentGroups = new Map<number, number[]>();\n\n for (const timeMs of timestamps) {\n try {\n const segmentId = this.mediaEngine.computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n if (!segmentGroups.has(segmentId)) {\n segmentGroups.set(segmentId, []);\n }\n const segmentGroup = segmentGroups.get(segmentId) ?? [];\n if (!segmentGroup) {\n segmentGroups.set(segmentId, []);\n }\n segmentGroup.push(timeMs);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`,\n error,\n );\n }\n }\n\n return segmentGroups;\n }\n\n /**\n * Extract thumbnails for a specific segment using CanvasSink\n */\n private async extractSegmentThumbnails(\n segmentId: number,\n timestamps: number[],\n rendition: VideoRendition,\n signal?: AbortSignal,\n ): Promise<Map<number, ThumbnailResult | null>> {\n const results = new Map<number, ThumbnailResult | null>();\n\n try {\n // Check abort before starting segment fetch\n signal?.throwIfAborted();\n \n // Get segment data through existing media engine methods (uses caches)\n // Note: fetchInitSegment requires a signal, so signal must be provided\n // If no signal provided, we cannot abort the operation, so skip it\n if (!signal) {\n // Return nulls for all timestamps when signal is not provided\n // This ensures we don't create orphan signals that can never be aborted\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n \n const [initSegment, mediaSegment] = await Promise.all([\n this.mediaEngine.fetchInitSegment(rendition, signal),\n this.mediaEngine.fetchMediaSegment(segmentId, rendition, signal),\n ]);\n \n // Check abort after potentially slow network operations\n signal?.throwIfAborted();\n\n // Create Input for this segment using global shared cache\n const segmentBlob = new Blob([initSegment, mediaSegment]);\n\n let input = globalInputCache.get(rendition.src, segmentId, rendition.id);\n if (!input) {\n input = new Input({\n formats: ALL_FORMATS,\n source: new BlobSource(segmentBlob),\n });\n globalInputCache.set(rendition.src, segmentId, input, rendition.id);\n }\n\n // Set up CanvasSink for batched extraction\n const videoTrack = await input.getPrimaryVideoTrack();\n if (!videoTrack) {\n // No video track - return nulls for all timestamps\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n\n const sink = new CanvasSink(videoTrack);\n\n // IMPORTANT: Sort timestamps for mediabunny - it expects monotonically sorted timestamps\n // Create array of {original, sorted} to map back after extraction\n const sortedTimestamps = [...timestamps].sort((a, b) => a - b);\n\n // Convert sorted global timestamps to segment-relative (in seconds for mediabunny)\n const relativeTimestamps = this.convertToSegmentRelativeTimestamps(\n sortedTimestamps,\n segmentId,\n rendition,\n );\n\n // Batch extract all thumbnails for this segment (in sorted order)\n const timestampResults = [];\n for await (const result of sink.canvasesAtTimestamps(\n relativeTimestamps,\n )) {\n timestampResults.push(result);\n }\n\n // Map results back to original (sorted) timestamps\n for (let i = 0; i < sortedTimestamps.length; i++) {\n const globalTimestamp = sortedTimestamps[i];\n if (globalTimestamp === undefined) {\n continue;\n }\n\n const result = timestampResults[i];\n\n if (result?.canvas) {\n const canvas = result.canvas;\n if (\n canvas instanceof HTMLCanvasElement ||\n canvas instanceof OffscreenCanvas\n ) {\n results.set(globalTimestamp, {\n timestamp: globalTimestamp,\n thumbnail: canvas,\n });\n } else {\n results.set(globalTimestamp, null);\n }\n } else {\n results.set(globalTimestamp, null);\n }\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Thumbnail extraction can fail for various non-fatal reasons (network issues, \n // missing segments, transcoding not ready). Log as warning and return nulls.\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Return nulls for all timestamps on error\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n }\n\n return results;\n }\n\n /**\n * Convert global timestamps to segment-relative timestamps for mediabunny\n * This is where the main difference between JIT and Asset engines lies\n */\n private convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n return this.mediaEngine.convertToSegmentRelativeTimestamps(\n globalTimestamps,\n segmentId,\n rendition,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -6,30 +6,47 @@ import { Task } from "@lit/task";
|
|
|
6
6
|
|
|
7
7
|
//#region src/elements/EFMedia/tasks/makeMediaEngineTask.ts
|
|
8
8
|
const getLatestMediaEngine = async (host, signal) => {
|
|
9
|
-
|
|
9
|
+
let mediaEngine;
|
|
10
|
+
try {
|
|
11
|
+
mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
14
|
+
if (error instanceof Error && error.message === "No valid media source") return;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
10
17
|
signal.throwIfAborted();
|
|
11
|
-
|
|
12
|
-
return mediaEngine;
|
|
18
|
+
return mediaEngine || void 0;
|
|
13
19
|
};
|
|
14
20
|
/**
|
|
15
21
|
* Core logic for creating a MediaEngine with explicit dependencies.
|
|
16
22
|
* Pure function that requires all dependencies to be provided.
|
|
23
|
+
*
|
|
24
|
+
* @param host - The EFMedia element host
|
|
25
|
+
* @param signal - AbortSignal to cancel in-flight requests when element is disconnected
|
|
17
26
|
*/
|
|
18
|
-
const createMediaEngine = (host) => {
|
|
19
|
-
const { src, assetId, urlGenerator, apiHost } = host;
|
|
27
|
+
const createMediaEngine = (host, signal) => {
|
|
28
|
+
const { src, assetId, urlGenerator, apiHost, requiredTracks } = host;
|
|
20
29
|
if (assetId !== null && assetId !== void 0 && assetId.trim() !== "") {
|
|
21
30
|
if (!apiHost) return Promise.reject(/* @__PURE__ */ new Error("API host is required for AssetID mode"));
|
|
22
|
-
return AssetIdMediaEngine.fetchByAssetId(host, urlGenerator, assetId, apiHost);
|
|
31
|
+
return AssetIdMediaEngine.fetchByAssetId(host, urlGenerator, assetId, apiHost, requiredTracks, signal);
|
|
23
32
|
}
|
|
24
33
|
if (!src || typeof src !== "string" || src.trim() === "") {
|
|
25
34
|
console.error(`Unsupported media source: ${src}, assetId: ${assetId}`);
|
|
26
35
|
return Promise.reject(/* @__PURE__ */ new Error("Unsupported media source"));
|
|
27
36
|
}
|
|
28
37
|
const lowerSrc = src.toLowerCase();
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
const isRemoteUrl = lowerSrc.startsWith("http://") || lowerSrc.startsWith("https://");
|
|
39
|
+
const configuration = host.closest("ef-configuration");
|
|
40
|
+
if (configuration?.mediaEngine === "jit") {
|
|
41
|
+
let manifestSrc = src;
|
|
42
|
+
if (!isRemoteUrl && configuration.apiHost) manifestSrc = `${configuration.apiHost.replace(/\/$/, "")}${src.replace(/^\.\//, "/src/")}`;
|
|
43
|
+
const url$1 = urlGenerator.generateManifestUrl(manifestSrc);
|
|
44
|
+
return JitMediaEngine.fetch(host, urlGenerator, url$1, signal);
|
|
45
|
+
}
|
|
46
|
+
if (configuration?.mediaEngine === "local") return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);
|
|
47
|
+
if (!isRemoteUrl) return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);
|
|
31
48
|
const url = urlGenerator.generateManifestUrl(src);
|
|
32
|
-
return JitMediaEngine.fetch(host, urlGenerator, url);
|
|
49
|
+
return JitMediaEngine.fetch(host, urlGenerator, url, signal);
|
|
33
50
|
};
|
|
34
51
|
/**
|
|
35
52
|
* Handle completion of media engine task - triggers necessary updates.
|
|
@@ -38,20 +55,34 @@ const createMediaEngine = (host) => {
|
|
|
38
55
|
const handleMediaEngineComplete = (host) => {
|
|
39
56
|
host.requestUpdate("intrinsicDurationMs");
|
|
40
57
|
host.requestUpdate("ownCurrentTimeMs");
|
|
41
|
-
host.rootTimegroup
|
|
42
|
-
|
|
58
|
+
if (host.rootTimegroup) queueMicrotask(() => {
|
|
59
|
+
host.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
|
|
60
|
+
host.rootTimegroup?.requestUpdate("durationMs");
|
|
61
|
+
});
|
|
43
62
|
};
|
|
44
63
|
const makeMediaEngineTask = (host) => {
|
|
45
|
-
|
|
64
|
+
let task;
|
|
65
|
+
task = new Task(host, {
|
|
46
66
|
autoRun: EF_INTERACTIVE,
|
|
47
67
|
args: () => [host.src, host.assetId],
|
|
48
|
-
task: async () => {
|
|
49
|
-
|
|
68
|
+
task: async ([_src, _assetId], { signal }) => {
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
const { src, assetId } = host;
|
|
71
|
+
if (assetId !== null && assetId !== void 0 && assetId.trim() !== "") return createMediaEngine(host, signal);
|
|
72
|
+
if (!src || typeof src !== "string" || src.trim() === "") return;
|
|
73
|
+
signal?.throwIfAborted();
|
|
74
|
+
return createMediaEngine(host, signal);
|
|
75
|
+
},
|
|
76
|
+
onError: (error) => {
|
|
77
|
+
task.taskComplete.catch(() => {});
|
|
78
|
+
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message.includes("signal is aborted") || error.message.includes("The user aborted a request")) || error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("404") || error.message.includes("Failed to fetch"))) return;
|
|
79
|
+
console.error("mediaEngineTask error", error);
|
|
50
80
|
},
|
|
51
|
-
onComplete: (
|
|
52
|
-
handleMediaEngineComplete(host);
|
|
81
|
+
onComplete: (value) => {
|
|
82
|
+
if (value) handleMediaEngineComplete(host);
|
|
53
83
|
}
|
|
54
84
|
});
|
|
85
|
+
return task;
|
|
55
86
|
};
|
|
56
87
|
|
|
57
88
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"makeMediaEngineTask.js","names":[],"sources":["../../../../src/elements/EFMedia/tasks/makeMediaEngineTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport { EF_INTERACTIVE } from \"../../../EF_INTERACTIVE\";\nimport type { MediaEngine } from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\nimport { AssetIdMediaEngine } from \"../AssetIdMediaEngine\";\nimport { AssetMediaEngine } from \"../AssetMediaEngine\";\nimport { JitMediaEngine } from \"../JitMediaEngine\";\n\nexport const getLatestMediaEngine = async (\n host: EFMedia,\n signal: AbortSignal,\n): Promise<MediaEngine> => {\n const mediaEngine = await host.mediaEngineTask.taskComplete;\n signal.throwIfAborted();\n if (!mediaEngine) {\n throw new Error(\"Media engine is not available\");\n }\n return mediaEngine;\n};\n\n/**\n * Core logic for creating a MediaEngine with explicit dependencies.\n * Pure function that requires all dependencies to be provided.\n */\nexport const createMediaEngine = (host: EFMedia): Promise<MediaEngine> => {\n const { src, assetId, urlGenerator, apiHost } = host;\n\n // Check for AssetID mode first\n if (assetId !== null && assetId !== undefined && assetId.trim() !== \"\") {\n if (!apiHost) {\n return Promise.reject(new Error(\"API host is required for AssetID mode\"));\n }\n return AssetIdMediaEngine.fetchByAssetId(\n host,\n urlGenerator,\n assetId,\n apiHost,\n );\n }\n\n // Check for null/undefined/empty/whitespace src\n if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n console.error(`Unsupported media source: ${src}, assetId: ${assetId}`);\n return Promise.reject(new Error(\"Unsupported media source\"));\n }\n\n const lowerSrc = src.toLowerCase();\n if (!lowerSrc.startsWith(\"http://\") && !lowerSrc.startsWith(\"https://\")) {\n return AssetMediaEngine.fetch(host, urlGenerator, src);\n }\n\n // Remote (http/https) source, now check configuration\n const configuration = host.closest(\"ef-configuration\");\n if (configuration?.mediaEngine === \"local\") {\n // Only use AssetMediaEngine for remote URLs when explicitly configured\n return AssetMediaEngine.fetch(host, urlGenerator, src);\n }\n\n // Default: Use JitMediaEngine for remote URLs (transcoding service)\n const url = urlGenerator.generateManifestUrl(src);\n return JitMediaEngine.fetch(host, urlGenerator, url);\n};\n\n/**\n * Handle completion of media engine task - triggers necessary updates.\n * Extracted for testability.\n */\nexport const handleMediaEngineComplete = (host: EFMedia): void => {\n host.requestUpdate(\"intrinsicDurationMs\");\n host.requestUpdate(\"ownCurrentTimeMs\");\n host.rootTimegroup?.requestUpdate(\"ownCurrentTimeMs\");\n host.rootTimegroup?.requestUpdate(\"durationMs\");\n};\n\ntype MediaEngineTask = Task<readonly [string, string | null], MediaEngine>;\n\nexport const makeMediaEngineTask = (host: EFMedia): MediaEngineTask => {\n return new Task(host, {\n autoRun: EF_INTERACTIVE,\n args: () => [host.src, host.assetId] as const,\n task: async () => {\n return createMediaEngine(host);\n },\n onComplete: (_value) => {\n handleMediaEngineComplete(host);\n },\n });\n};\n"],"mappings":";;;;;;;AAQA,MAAa,uBAAuB,OAClC,MACA,WACyB;CACzB,MAAM,cAAc,MAAM,KAAK,gBAAgB;AAC/C,QAAO,gBAAgB;AACvB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gCAAgC;AAElD,QAAO;;;;;;AAOT,MAAa,qBAAqB,SAAwC;CACxE,MAAM,EAAE,KAAK,SAAS,cAAc,YAAY;AAGhD,KAAI,YAAY,QAAQ,YAAY,UAAa,QAAQ,MAAM,KAAK,IAAI;AACtE,MAAI,CAAC,QACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,wCAAwC,CAAC;AAE3E,SAAO,mBAAmB,eACxB,MACA,cACA,SACA,QACD;;AAIH,KAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,IAAI,MAAM,KAAK,IAAI;AACxD,UAAQ,MAAM,6BAA6B,IAAI,aAAa,UAAU;AACtE,SAAO,QAAQ,uBAAO,IAAI,MAAM,2BAA2B,CAAC;;CAG9D,MAAM,WAAW,IAAI,aAAa;AAClC,KAAI,CAAC,SAAS,WAAW,UAAU,IAAI,CAAC,SAAS,WAAW,WAAW,CACrE,QAAO,iBAAiB,MAAM,MAAM,cAAc,IAAI;AAKxD,KADsB,KAAK,QAAQ,mBAAmB,EACnC,gBAAgB,QAEjC,QAAO,iBAAiB,MAAM,MAAM,cAAc,IAAI;CAIxD,MAAM,MAAM,aAAa,oBAAoB,IAAI;AACjD,QAAO,eAAe,MAAM,MAAM,cAAc,IAAI;;;;;;AAOtD,MAAa,6BAA6B,SAAwB;AAChE,MAAK,cAAc,sBAAsB;AACzC,MAAK,cAAc,mBAAmB;AACtC,MAAK,eAAe,cAAc,mBAAmB;AACrD,MAAK,eAAe,cAAc,aAAa;;AAKjD,MAAa,uBAAuB,SAAmC;AACrE,QAAO,IAAI,KAAK,MAAM;EACpB,SAAS;EACT,YAAY,CAAC,KAAK,KAAK,KAAK,QAAQ;EACpC,MAAM,YAAY;AAChB,UAAO,kBAAkB,KAAK;;EAEhC,aAAa,WAAW;AACtB,6BAA0B,KAAK;;EAElC,CAAC"}
|
|
1
|
+
{"version":3,"file":"makeMediaEngineTask.js","names":["url","task: MediaEngineTask"],"sources":["../../../../src/elements/EFMedia/tasks/makeMediaEngineTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport { EF_INTERACTIVE } from \"../../../EF_INTERACTIVE\";\nimport type { MediaEngine } from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\nimport { AssetIdMediaEngine } from \"../AssetIdMediaEngine\";\nimport { AssetMediaEngine } from \"../AssetMediaEngine\";\nimport { JitMediaEngine } from \"../JitMediaEngine\";\n\nexport const getLatestMediaEngine = async (\n host: EFMedia,\n signal: AbortSignal,\n): Promise<MediaEngine | undefined> => {\n let mediaEngine;\n try {\n mediaEngine = await host.mediaEngineTask.taskComplete;\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If the error is \"No valid media source\", return undefined instead of throwing\n // This allows callers to handle missing media gracefully\n if (error instanceof Error && error.message === \"No valid media source\") {\n return undefined;\n }\n // For other errors, re-throw\n throw error;\n }\n signal.throwIfAborted();\n // Return undefined if no media engine (no valid source)\n // Callers should check for undefined and exit gracefully\n return mediaEngine || undefined;\n};\n\n/**\n * Core logic for creating a MediaEngine with explicit dependencies.\n * Pure function that requires all dependencies to be provided.\n * \n * @param host - The EFMedia element host\n * @param signal - AbortSignal to cancel in-flight requests when element is disconnected\n */\nexport const createMediaEngine = (host: EFMedia, signal?: AbortSignal): Promise<MediaEngine> => {\n const { src, assetId, urlGenerator, apiHost, requiredTracks } = host;\n\n // Check for AssetID mode first\n if (assetId !== null && assetId !== undefined && assetId.trim() !== \"\") {\n if (!apiHost) {\n return Promise.reject(new Error(\"API host is required for AssetID mode\"));\n }\n return AssetIdMediaEngine.fetchByAssetId(\n host,\n urlGenerator,\n assetId,\n apiHost,\n requiredTracks,\n signal,\n );\n }\n\n // Check for null/undefined/empty/whitespace src\n if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n console.error(`Unsupported media source: ${src}, assetId: ${assetId}`);\n return Promise.reject(new Error(\"Unsupported media source\"));\n }\n\n const lowerSrc = src.toLowerCase();\n const isRemoteUrl = lowerSrc.startsWith(\"http://\") || lowerSrc.startsWith(\"https://\");\n \n // Check configuration for explicit engine preference\n const configuration = host.closest(\"ef-configuration\");\n \n // \"jit\" mode: Force JitMediaEngine for all sources (including local files)\n if (configuration?.mediaEngine === \"jit\") {\n // For local paths, convert to full URL using apiHost\n let manifestSrc = src;\n if (!isRemoteUrl && configuration.apiHost) {\n // Convert relative path to absolute URL for the JIT manifest\n // e.g., \"./assets/video.mp4\" -> \"http://main.localhost:4321/src/assets/video.mp4\"\n const baseUrl = configuration.apiHost.replace(/\\/$/, \"\");\n const normalizedPath = src.replace(/^\\.\\//, \"/src/\");\n manifestSrc = `${baseUrl}${normalizedPath}`;\n }\n const url = urlGenerator.generateManifestUrl(manifestSrc);\n return JitMediaEngine.fetch(host, urlGenerator, url, signal);\n }\n \n // \"local\" mode: Force AssetMediaEngine for all sources\n if (configuration?.mediaEngine === \"local\") {\n return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);\n }\n \n // \"cloud\" mode (default): AssetMediaEngine for local paths, JitMediaEngine for remote URLs\n if (!isRemoteUrl) {\n return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);\n }\n\n // Default: Use JitMediaEngine for remote URLs (transcoding service)\n const url = urlGenerator.generateManifestUrl(src);\n return JitMediaEngine.fetch(host, urlGenerator, url, signal);\n};\n\n/**\n * Handle completion of media engine task - triggers necessary updates.\n * Extracted for testability.\n */\nexport const handleMediaEngineComplete = (host: EFMedia): void => {\n // Update self synchronously - this is fine because we're updating the element\n // that just completed its task, not a parent\n host.requestUpdate(\"intrinsicDurationMs\");\n host.requestUpdate(\"ownCurrentTimeMs\");\n \n // Defer updates to parent/root timegroup to avoid Lit warning about scheduling\n // updates after update completed (change-in-update). Task onComplete can be\n // called during an update cycle, and directly calling requestUpdate on parent\n // elements causes the warning.\n if (host.rootTimegroup) {\n queueMicrotask(() => {\n host.rootTimegroup?.requestUpdate(\"ownCurrentTimeMs\");\n host.rootTimegroup?.requestUpdate(\"durationMs\");\n });\n }\n};\n\ntype MediaEngineTask = Task<readonly [string, string | null], MediaEngine>;\n\nexport const makeMediaEngineTask = (host: EFMedia): MediaEngineTask => {\n // Capture task reference for use in onError\n let task: MediaEngineTask;\n \n task = new Task(host, {\n autoRun: EF_INTERACTIVE,\n args: () => [host.src, host.assetId] as const,\n task: async ([_src, _assetId], { signal }) => {\n // Check abort before starting work\n signal?.throwIfAborted();\n \n // Check if we have a valid source before attempting to create media engine\n // This avoids unnecessary errors when src is empty/null/undefined\n const { src, assetId } = host;\n \n // If we have a valid assetId, proceed\n if (assetId !== null && assetId !== undefined && assetId.trim() !== \"\") {\n return createMediaEngine(host, signal);\n }\n \n // If we don't have a valid src, return undefined instead of throwing\n // This allows dependent tasks to check for undefined and exit gracefully\n // without logging errors for expected conditions\n if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n return undefined as unknown as MediaEngine;\n }\n \n // Check abort before expensive operation\n signal?.throwIfAborted();\n \n return createMediaEngine(host, signal);\n },\n onError: (error) => {\n // CRITICAL: Attach .catch() handler to taskComplete BEFORE the promise is rejected.\n // onError is called synchronously before completeDeferred.reject() in Lit Task,\n // so this handler will be attached in time to prevent unhandled rejection.\n // Without this, the rejection from hostUpdate() -> _performTask() (which isn't awaited)\n // becomes an unhandled promise rejection.\n task.taskComplete.catch(() => {});\n \n // Don't log AbortError - these are intentional cancellations when element is disconnected\n const isAbortError = \n error instanceof DOMException && error.name === \"AbortError\" ||\n error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")\n );\n \n // Don't log errors when there's no valid media source or file not found - these are expected\n if (isAbortError || (error instanceof Error && (\n error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"404\") ||\n error.message.includes(\"Failed to fetch\")\n ))) {\n return;\n }\n \n // Log other unexpected errors\n console.error(\"mediaEngineTask error\", error);\n },\n onComplete: (value) => {\n // Only trigger updates if we actually got a media engine\n if (value) {\n handleMediaEngineComplete(host);\n }\n },\n });\n \n return task;\n};\n"],"mappings":"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"}
|
|
@@ -5,45 +5,63 @@
|
|
|
5
5
|
* for multiple frames within that segment (e.g., 60 frames at 30fps)
|
|
6
6
|
*/
|
|
7
7
|
var MainVideoInputCache = class {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
8
|
+
#cache = /* @__PURE__ */ new Map();
|
|
9
|
+
#pendingPromises = /* @__PURE__ */ new Map();
|
|
10
|
+
#maxCacheSize = 10;
|
|
12
11
|
/**
|
|
13
12
|
* Create a cache key that uniquely identifies a segment
|
|
14
13
|
*/
|
|
15
|
-
getCacheKey(src, segmentId, renditionId) {
|
|
14
|
+
#getCacheKey(src, segmentId, renditionId) {
|
|
16
15
|
return `${src}:${renditionId || "default"}:${segmentId}`;
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
|
-
* Get or create BufferedSeekingInput for a main video segment
|
|
18
|
+
* Get or create BufferedSeekingInput for a main video segment.
|
|
19
|
+
*
|
|
20
|
+
* Uses promise deduplication to prevent race conditions when multiple
|
|
21
|
+
* concurrent requests arrive for the same segment. Without this,
|
|
22
|
+
* the first segment often fails when DevTools is closed because:
|
|
23
|
+
* 1. Video display and thumbnail extraction both request segment 0
|
|
24
|
+
* 2. Both find cache empty and start createInputFn()
|
|
25
|
+
* 3. Both create separate instances, causing conflicts
|
|
20
26
|
*/
|
|
21
27
|
async getOrCreateInput(src, segmentId, renditionId, createInputFn) {
|
|
22
|
-
const cacheKey = this
|
|
23
|
-
const cached = this
|
|
28
|
+
const cacheKey = this.#getCacheKey(src, segmentId, renditionId);
|
|
29
|
+
const cached = this.#cache.get(cacheKey);
|
|
24
30
|
if (cached) return cached;
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const pending = this.#pendingPromises.get(cacheKey);
|
|
32
|
+
if (pending) return pending;
|
|
33
|
+
const promise = createInputFn().then((input) => {
|
|
34
|
+
this.#pendingPromises.delete(cacheKey);
|
|
35
|
+
if (input) {
|
|
36
|
+
this.#cache.set(cacheKey, input);
|
|
37
|
+
if (this.#cache.size > this.#maxCacheSize) {
|
|
38
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
39
|
+
if (oldestKey !== void 0) this.#cache.delete(oldestKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}).catch((error) => {
|
|
44
|
+
this.#pendingPromises.delete(cacheKey);
|
|
45
|
+
throw error;
|
|
46
|
+
});
|
|
47
|
+
this.#pendingPromises.set(cacheKey, promise);
|
|
48
|
+
return promise;
|
|
33
49
|
}
|
|
34
50
|
/**
|
|
35
51
|
* Clear the entire cache (called when video changes)
|
|
36
52
|
*/
|
|
37
53
|
clear() {
|
|
38
|
-
this
|
|
54
|
+
this.#cache.clear();
|
|
55
|
+
this.#pendingPromises.clear();
|
|
39
56
|
}
|
|
40
57
|
/**
|
|
41
58
|
* Get cache statistics
|
|
42
59
|
*/
|
|
43
60
|
getStats() {
|
|
44
61
|
return {
|
|
45
|
-
size: this
|
|
46
|
-
|
|
62
|
+
size: this.#cache.size,
|
|
63
|
+
pendingSize: this.#pendingPromises.size,
|
|
64
|
+
cacheKeys: Array.from(this.#cache.keys())
|
|
47
65
|
};
|
|
48
66
|
}
|
|
49
67
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MainVideoInputCache.js","names":[],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n
|
|
1
|
+
{"version":3,"file":"MainVideoInputCache.js","names":["#getCacheKey","#cache","#pendingPromises","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n #cache = new Map<string, BufferedSeekingInput>();\n #pendingPromises = new Map<string, Promise<BufferedSeekingInput | undefined>>();\n #maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)\n\n /**\n * Create a cache key that uniquely identifies a segment\n */\n #getCacheKey(\n src: string,\n segmentId: number,\n renditionId: string | undefined,\n ): string {\n return `${src}:${renditionId || \"default\"}:${segmentId}`;\n }\n\n /**\n * Get or create BufferedSeekingInput for a main video segment.\n * \n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment. Without this,\n * the first segment often fails when DevTools is closed because:\n * 1. Video display and thumbnail extraction both request segment 0\n * 2. Both find cache empty and start createInputFn()\n * 3. Both create separate instances, causing conflicts\n */\n async getOrCreateInput(\n src: string,\n segmentId: number,\n renditionId: string | undefined,\n createInputFn: () => Promise<BufferedSeekingInput | undefined>,\n ): Promise<BufferedSeekingInput | undefined> {\n const cacheKey = this.#getCacheKey(src, segmentId, renditionId);\n\n // Check if we already have a completed result cached\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Check if there's already a pending request for this segment (deduplication!)\n // This prevents the race condition where multiple concurrent requests\n // each create their own BufferedSeekingInput instance.\n const pending = this.#pendingPromises.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n // Create the promise and cache it IMMEDIATELY to prevent race conditions\n const promise = createInputFn().then((input) => {\n // Clean up pending promise\n this.#pendingPromises.delete(cacheKey);\n \n if (input) {\n // Add to completed cache\n this.#cache.set(cacheKey, input);\n\n // Evict oldest entries if cache is too large (LRU-like behavior)\n if (this.#cache.size > this.#maxCacheSize) {\n const oldestKey = this.#cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.#cache.delete(oldestKey);\n }\n }\n }\n \n return input;\n }).catch((error) => {\n // Clean up pending promise on failure so retry is possible\n this.#pendingPromises.delete(cacheKey);\n throw error;\n });\n\n this.#pendingPromises.set(cacheKey, promise);\n return promise;\n }\n\n /**\n * Clear the entire cache (called when video changes)\n */\n clear() {\n this.#cache.clear();\n this.#pendingPromises.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.#cache.size,\n pendingSize: this.#pendingPromises.size,\n cacheKeys: Array.from(this.#cache.keys()),\n };\n }\n}\n"],"mappings":";;;;;;AAOA,IAAa,sBAAb,MAAiC;CAC/B,yBAAS,IAAI,KAAmC;CAChD,mCAAmB,IAAI,KAAwD;CAC/E,gBAAgB;;;;CAKhB,aACE,KACA,WACA,aACQ;AACR,SAAO,GAAG,IAAI,GAAG,eAAe,UAAU,GAAG;;;;;;;;;;;;CAa/C,MAAM,iBACJ,KACA,WACA,aACA,eAC2C;EAC3C,MAAM,WAAW,MAAKA,YAAa,KAAK,WAAW,YAAY;EAG/D,MAAM,SAAS,MAAKC,MAAO,IAAI,SAAS;AACxC,MAAI,OACF,QAAO;EAMT,MAAM,UAAU,MAAKC,gBAAiB,IAAI,SAAS;AACnD,MAAI,QACF,QAAO;EAIT,MAAM,UAAU,eAAe,CAAC,MAAM,UAAU;AAE9C,SAAKA,gBAAiB,OAAO,SAAS;AAEtC,OAAI,OAAO;AAET,UAAKD,MAAO,IAAI,UAAU,MAAM;AAGhC,QAAI,MAAKA,MAAO,OAAO,MAAKE,cAAe;KACzC,MAAM,YAAY,MAAKF,MAAO,MAAM,CAAC,MAAM,CAAC;AAC5C,SAAI,cAAc,OAChB,OAAKA,MAAO,OAAO,UAAU;;;AAKnC,UAAO;IACP,CAAC,OAAO,UAAU;AAElB,SAAKC,gBAAiB,OAAO,SAAS;AACtC,SAAM;IACN;AAEF,QAAKA,gBAAiB,IAAI,UAAU,QAAQ;AAC5C,SAAO;;;;;CAMT,QAAQ;AACN,QAAKD,MAAO,OAAO;AACnB,QAAKC,gBAAiB,OAAO;;;;;CAM/B,WAAW;AACT,SAAO;GACL,MAAM,MAAKD,MAAO;GAClB,aAAa,MAAKC,gBAAiB;GACnC,WAAW,MAAM,KAAK,MAAKD,MAAO,MAAM,CAAC;GAC1C"}
|
|
@@ -1,42 +1,86 @@
|
|
|
1
1
|
//#region src/elements/EFMedia/videoTasks/ScrubInputCache.ts
|
|
2
2
|
/**
|
|
3
|
-
* Cache for scrub BufferedSeekingInput instances
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Cache for scrub BufferedSeekingInput instances.
|
|
4
|
+
*
|
|
5
|
+
* For JIT media (segmented scrub tracks), caches by segment ID.
|
|
6
|
+
* For Asset media (single-file scrub tracks), caches by URL so all segments
|
|
7
|
+
* share the same BufferedSeekingInput instance.
|
|
8
|
+
*
|
|
9
|
+
* Uses promise deduplication to prevent race conditions when multiple
|
|
10
|
+
* concurrent requests arrive for the same segment.
|
|
6
11
|
*/
|
|
7
12
|
var ScrubInputCache = class {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
#cache = /* @__PURE__ */ new Map();
|
|
14
|
+
#urlCache = /* @__PURE__ */ new Map();
|
|
15
|
+
#pendingBySegment = /* @__PURE__ */ new Map();
|
|
16
|
+
#pendingByUrl = /* @__PURE__ */ new Map();
|
|
17
|
+
#maxCacheSize = 5;
|
|
12
18
|
/**
|
|
13
|
-
* Get or create BufferedSeekingInput for a scrub segment
|
|
19
|
+
* Get or create BufferedSeekingInput for a scrub segment.
|
|
20
|
+
*
|
|
21
|
+
* Uses promise deduplication to prevent race conditions when multiple
|
|
22
|
+
* concurrent requests arrive for the same segment.
|
|
23
|
+
*
|
|
24
|
+
* @param segmentId - The segment ID
|
|
25
|
+
* @param createInputFn - Factory function to create the input
|
|
26
|
+
* @param scrubUrl - Optional URL for single-file scrub tracks (all segments share same input)
|
|
14
27
|
*/
|
|
15
|
-
async getOrCreateInput(segmentId, createInputFn) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
async getOrCreateInput(segmentId, createInputFn, scrubUrl) {
|
|
29
|
+
if (scrubUrl) {
|
|
30
|
+
const cached$1 = this.#urlCache.get(scrubUrl);
|
|
31
|
+
if (cached$1) return cached$1;
|
|
32
|
+
const pending$1 = this.#pendingByUrl.get(scrubUrl);
|
|
33
|
+
if (pending$1) return pending$1;
|
|
34
|
+
const promise$1 = createInputFn().then((input) => {
|
|
35
|
+
this.#pendingByUrl.delete(scrubUrl);
|
|
36
|
+
if (input) this.#urlCache.set(scrubUrl, input);
|
|
37
|
+
return input;
|
|
38
|
+
}).catch((error) => {
|
|
39
|
+
this.#pendingByUrl.delete(scrubUrl);
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
this.#pendingByUrl.set(scrubUrl, promise$1);
|
|
43
|
+
return promise$1;
|
|
24
44
|
}
|
|
25
|
-
|
|
45
|
+
const cached = this.#cache.get(segmentId);
|
|
46
|
+
if (cached) return cached;
|
|
47
|
+
const pending = this.#pendingBySegment.get(segmentId);
|
|
48
|
+
if (pending) return pending;
|
|
49
|
+
const promise = createInputFn().then((input) => {
|
|
50
|
+
this.#pendingBySegment.delete(segmentId);
|
|
51
|
+
if (input) {
|
|
52
|
+
this.#cache.set(segmentId, input);
|
|
53
|
+
if (this.#cache.size > this.#maxCacheSize) {
|
|
54
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
55
|
+
if (oldestKey !== void 0) this.#cache.delete(oldestKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return input;
|
|
59
|
+
}).catch((error) => {
|
|
60
|
+
this.#pendingBySegment.delete(segmentId);
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
this.#pendingBySegment.set(segmentId, promise);
|
|
64
|
+
return promise;
|
|
26
65
|
}
|
|
27
66
|
/**
|
|
28
67
|
* Clear the entire cache (called when video changes)
|
|
29
68
|
*/
|
|
30
69
|
clear() {
|
|
31
|
-
this
|
|
70
|
+
this.#cache.clear();
|
|
71
|
+
this.#urlCache.clear();
|
|
72
|
+
this.#pendingBySegment.clear();
|
|
73
|
+
this.#pendingByUrl.clear();
|
|
32
74
|
}
|
|
33
75
|
/**
|
|
34
76
|
* Get cache statistics
|
|
35
77
|
*/
|
|
36
78
|
getStats() {
|
|
37
79
|
return {
|
|
38
|
-
size: this
|
|
39
|
-
|
|
80
|
+
size: this.#cache.size,
|
|
81
|
+
urlCacheSize: this.#urlCache.size,
|
|
82
|
+
pendingCount: this.#pendingBySegment.size + this.#pendingByUrl.size,
|
|
83
|
+
segmentIds: Array.from(this.#cache.keys())
|
|
40
84
|
};
|
|
41
85
|
}
|
|
42
86
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScrubInputCache.js","names":[],"sources":["../../../../src/elements/EFMedia/videoTasks/ScrubInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for scrub BufferedSeekingInput instances\n *
|
|
1
|
+
{"version":3,"file":"ScrubInputCache.js","names":["cached","#urlCache","pending","#pendingByUrl","promise","#cache","#pendingBySegment","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/ScrubInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for scrub BufferedSeekingInput instances.\n * \n * For JIT media (segmented scrub tracks), caches by segment ID.\n * For Asset media (single-file scrub tracks), caches by URL so all segments\n * share the same BufferedSeekingInput instance.\n * \n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment.\n */\nexport class ScrubInputCache {\n #cache = new Map<number, BufferedSeekingInput>();\n #urlCache = new Map<string, BufferedSeekingInput>();\n #pendingBySegment = new Map<number, Promise<BufferedSeekingInput | undefined>>();\n #pendingByUrl = new Map<string, Promise<BufferedSeekingInput | undefined>>();\n #maxCacheSize = 5;\n\n /**\n * Get or create BufferedSeekingInput for a scrub segment.\n * \n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment.\n * \n * @param segmentId - The segment ID\n * @param createInputFn - Factory function to create the input\n * @param scrubUrl - Optional URL for single-file scrub tracks (all segments share same input)\n */\n async getOrCreateInput(\n segmentId: number,\n createInputFn: () => Promise<BufferedSeekingInput | undefined>,\n scrubUrl?: string,\n ): Promise<BufferedSeekingInput | undefined> {\n // For single-file scrub tracks (AssetMediaEngine), use URL-based caching\n // This ensures all segments share the same BufferedSeekingInput\n if (scrubUrl) {\n // Check completed cache\n const cached = this.#urlCache.get(scrubUrl);\n if (cached) {\n return cached;\n }\n\n // Check pending requests (deduplication)\n const pending = this.#pendingByUrl.get(scrubUrl);\n if (pending) {\n return pending;\n }\n\n // Create promise and cache immediately\n const promise = createInputFn().then((input) => {\n this.#pendingByUrl.delete(scrubUrl);\n if (input) {\n this.#urlCache.set(scrubUrl, input);\n }\n return input;\n }).catch((error) => {\n this.#pendingByUrl.delete(scrubUrl);\n throw error;\n });\n\n this.#pendingByUrl.set(scrubUrl, promise);\n return promise;\n }\n\n // For segmented scrub tracks (JIT), use segment-based caching\n const cached = this.#cache.get(segmentId);\n if (cached) {\n return cached;\n }\n\n // Check pending requests (deduplication)\n const pending = this.#pendingBySegment.get(segmentId);\n if (pending) {\n return pending;\n }\n\n // Create promise and cache immediately\n const promise = createInputFn().then((input) => {\n this.#pendingBySegment.delete(segmentId);\n \n if (input) {\n this.#cache.set(segmentId, input);\n\n // Evict oldest entries if cache is too large\n if (this.#cache.size > this.#maxCacheSize) {\n const oldestKey = this.#cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.#cache.delete(oldestKey);\n }\n }\n }\n \n return input;\n }).catch((error) => {\n this.#pendingBySegment.delete(segmentId);\n throw error;\n });\n\n this.#pendingBySegment.set(segmentId, promise);\n return promise;\n }\n\n /**\n * Clear the entire cache (called when video changes)\n */\n clear() {\n this.#cache.clear();\n this.#urlCache.clear();\n this.#pendingBySegment.clear();\n this.#pendingByUrl.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.#cache.size,\n urlCacheSize: this.#urlCache.size,\n pendingCount: this.#pendingBySegment.size + this.#pendingByUrl.size,\n segmentIds: Array.from(this.#cache.keys()),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAa,kBAAb,MAA6B;CAC3B,yBAAS,IAAI,KAAmC;CAChD,4BAAY,IAAI,KAAmC;CACnD,oCAAoB,IAAI,KAAwD;CAChF,gCAAgB,IAAI,KAAwD;CAC5E,gBAAgB;;;;;;;;;;;CAYhB,MAAM,iBACJ,WACA,eACA,UAC2C;AAG3C,MAAI,UAAU;GAEZ,MAAMA,WAAS,MAAKC,SAAU,IAAI,SAAS;AAC3C,OAAID,SACF,QAAOA;GAIT,MAAME,YAAU,MAAKC,aAAc,IAAI,SAAS;AAChD,OAAID,UACF,QAAOA;GAIT,MAAME,YAAU,eAAe,CAAC,MAAM,UAAU;AAC9C,UAAKD,aAAc,OAAO,SAAS;AACnC,QAAI,MACF,OAAKF,SAAU,IAAI,UAAU,MAAM;AAErC,WAAO;KACP,CAAC,OAAO,UAAU;AAClB,UAAKE,aAAc,OAAO,SAAS;AACnC,UAAM;KACN;AAEF,SAAKA,aAAc,IAAI,UAAUC,UAAQ;AACzC,UAAOA;;EAIT,MAAM,SAAS,MAAKC,MAAO,IAAI,UAAU;AACzC,MAAI,OACF,QAAO;EAIT,MAAM,UAAU,MAAKC,iBAAkB,IAAI,UAAU;AACrD,MAAI,QACF,QAAO;EAIT,MAAM,UAAU,eAAe,CAAC,MAAM,UAAU;AAC9C,SAAKA,iBAAkB,OAAO,UAAU;AAExC,OAAI,OAAO;AACT,UAAKD,MAAO,IAAI,WAAW,MAAM;AAGjC,QAAI,MAAKA,MAAO,OAAO,MAAKE,cAAe;KACzC,MAAM,YAAY,MAAKF,MAAO,MAAM,CAAC,MAAM,CAAC;AAC5C,SAAI,cAAc,OAChB,OAAKA,MAAO,OAAO,UAAU;;;AAKnC,UAAO;IACP,CAAC,OAAO,UAAU;AAClB,SAAKC,iBAAkB,OAAO,UAAU;AACxC,SAAM;IACN;AAEF,QAAKA,iBAAkB,IAAI,WAAW,QAAQ;AAC9C,SAAO;;;;;CAMT,QAAQ;AACN,QAAKD,MAAO,OAAO;AACnB,QAAKJ,SAAU,OAAO;AACtB,QAAKK,iBAAkB,OAAO;AAC9B,QAAKH,aAAc,OAAO;;;;;CAM5B,WAAW;AACT,SAAO;GACL,MAAM,MAAKE,MAAO;GAClB,cAAc,MAAKJ,SAAU;GAC7B,cAAc,MAAKK,iBAAkB,OAAO,MAAKH,aAAc;GAC/D,YAAY,MAAM,KAAK,MAAKE,MAAO,MAAM,CAAC;GAC3C"}
|
|
@@ -16,10 +16,14 @@ const makeScrubVideoBufferTask = (host) => {
|
|
|
16
16
|
activeRequests: /* @__PURE__ */ new Set(),
|
|
17
17
|
requestQueue: []
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
let task;
|
|
20
|
+
task = new Task(host, {
|
|
20
21
|
autoRun: EF_INTERACTIVE,
|
|
21
22
|
args: () => [host.mediaEngineTask.value],
|
|
22
23
|
onError: (error) => {
|
|
24
|
+
task.taskComplete.catch(() => {});
|
|
25
|
+
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
|
|
26
|
+
if (error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("is not valid JSON"))) return;
|
|
23
27
|
console.error("scrubVideoBufferTask error", error);
|
|
24
28
|
},
|
|
25
29
|
onComplete: (value) => {
|
|
@@ -51,7 +55,7 @@ const makeScrubVideoBufferTask = (host) => {
|
|
|
51
55
|
return mediaEngine.computeSegmentId(timeMs, rendition);
|
|
52
56
|
},
|
|
53
57
|
prefetchSegment: async (segmentId, rendition) => {
|
|
54
|
-
await mediaEngine.fetchMediaSegment(segmentId, rendition);
|
|
58
|
+
await mediaEngine.fetchMediaSegment(segmentId, rendition, signal);
|
|
55
59
|
},
|
|
56
60
|
isSegmentCached: (segmentId, rendition) => {
|
|
57
61
|
return mediaEngine.isSegmentCached(segmentId, rendition);
|
|
@@ -62,12 +66,13 @@ const makeScrubVideoBufferTask = (host) => {
|
|
|
62
66
|
}
|
|
63
67
|
});
|
|
64
68
|
} catch (error) {
|
|
65
|
-
if (signal
|
|
69
|
+
if (signal?.aborted) return currentState;
|
|
66
70
|
console.warn("ScrubBuffer failed:", error);
|
|
67
71
|
return currentState;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
});
|
|
75
|
+
return task;
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
//#endregion
|