@editframe/elements 0.37.3-beta → 0.38.1
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.js +17 -14
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.d.ts +9 -2
- package/dist/canvas/EFCanvas.js +14 -4
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +2 -2
- package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
- package/dist/canvas/overlays/SelectionOverlay.js +5 -12
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +1 -11
- package/dist/elements/EFAudio.js +2 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +5 -9
- package/dist/elements/EFCaptions.js +34 -11
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +10 -8
- package/dist/elements/EFImage.js +117 -32
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
- package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
- package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +14 -8
- package/dist/elements/EFMedia.js +52 -19
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +2 -2
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +16 -8
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +5 -8
- package/dist/elements/EFSurface.js +4 -43
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +33 -8
- package/dist/elements/EFTemporal.js +92 -40
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +3 -0
- package/dist/elements/EFText.js +54 -21
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.js +8 -4
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +26 -43
- package/dist/elements/EFTimegroup.js +295 -314
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +44 -42
- package/dist/elements/EFVideo.js +259 -172
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +3 -8
- package/dist/elements/EFWaveform.js +18 -13
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/TargetController.d.ts +0 -3
- package/dist/elements/TargetController.js +12 -35
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
- package/dist/elements/cloneFactoryRegistry.js +15 -0
- package/dist/elements/cloneFactoryRegistry.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +8 -6
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js +62 -0
- package/dist/elements/setupTemporalHierarchy.js.map +1 -0
- package/dist/elements/updateAnimations.js +62 -87
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +3 -2
- package/dist/getRenderInfo.js +20 -4
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js +68 -12
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +2 -2
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +2 -2
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +2 -2
- package/dist/gui/EFDial.js +12 -9
- package/dist/gui/EFDial.js.map +1 -1
- package/dist/gui/EFFilmstrip.d.ts +2 -0
- package/dist/gui/EFFilmstrip.js +18 -10
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +28 -4
- package/dist/gui/EFFitScale.js +88 -26
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +2 -2
- package/dist/gui/EFFocusOverlay.js +3 -3
- package/dist/gui/EFFocusOverlay.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +2 -2
- package/dist/gui/EFOverlayLayer.d.ts +2 -2
- package/dist/gui/EFPause.d.ts +2 -2
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +2 -2
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +2 -2
- package/dist/gui/EFResizableBox.js +5 -5
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +2 -2
- package/dist/gui/EFScrubber.js +8 -13
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +6 -2
- package/dist/gui/EFTimeDisplay.js +25 -7
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +2 -2
- package/dist/gui/EFTimelineRuler.js +3 -3
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFToggleLoop.d.ts +2 -2
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +2 -2
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +2 -2
- package/dist/gui/EFTransformHandles.js +6 -6
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +40 -36
- package/dist/gui/EFWorkbench.js +436 -822
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +3 -8
- package/dist/gui/PlaybackController.js +59 -56
- 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/TargetOrContextMixin.js +43 -6
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/ef-theme.css +136 -0
- package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchy.js +14 -24
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/icons.js.map +1 -1
- package/dist/gui/previewSettingsContext.d.ts +18 -0
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js +34 -0
- package/dist/gui/theme.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +70 -52
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +5 -3
- package/dist/gui/timeline/EFTimelineRow.js +55 -32
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +23 -9
- package/dist/gui/timeline/TrimHandles.js +224 -51
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
- package/dist/gui/timeline/timelineEditingContext.js +24 -0
- package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
- package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
- package/dist/gui/timeline/tracks/TextTrack.js +17 -43
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +3 -4
- package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
- package/dist/gui/timeline/tracks/TrackItem.js +18 -17
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
- package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +2 -2
- package/dist/gui/tree/EFTree.js +8 -14
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +2 -2
- package/dist/gui/tree/EFTreeItem.js +3 -3
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/gui/tree/treeContext.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/preview/AdaptiveResolutionTracker.js +3 -3
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +2 -17
- package/dist/preview/FrameController.js +40 -63
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
- package/dist/preview/QualityUpgradeScheduler.js +158 -0
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +119 -1
- package/dist/preview/RenderContext.js +21 -3
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js +85 -0
- package/dist/preview/RenderStats.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +2 -52
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/encoding/workerEncoder.js.map +1 -1
- package/dist/preview/logger.js.map +1 -1
- package/dist/preview/previewSettings.d.ts +34 -0
- package/dist/preview/previewSettings.js +29 -17
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js +4 -4
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.d.ts +44 -0
- package/dist/preview/renderElementToCanvas.js +72 -0
- package/dist/preview/renderElementToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +134 -32
- package/dist/preview/renderTimegroupToCanvas.js +321 -146
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +51 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +20 -35
- package/dist/preview/renderTimegroupToVideo.js +94 -106
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.types.d.ts +42 -0
- package/dist/preview/renderVideoToVideo.js +286 -0
- package/dist/preview/renderVideoToVideo.js.map +1 -0
- package/dist/preview/renderers.d.ts +56 -0
- package/dist/preview/renderers.js +13 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js +74 -0
- package/dist/preview/rendering/ScaleConfig.js.map +1 -0
- package/dist/preview/rendering/inlineImages.d.ts +13 -0
- package/dist/preview/rendering/inlineImages.js +7 -44
- package/dist/preview/rendering/inlineImages.js.map +1 -1
- package/dist/preview/rendering/loadImage.d.ts +8 -0
- package/dist/preview/rendering/loadImage.js +22 -0
- package/dist/preview/rendering/loadImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.js +3 -3
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js +1 -101
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +0 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/preview/workers/encoderWorkerInline.js +21 -54
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +2 -1
- package/dist/render/EFRenderAPI.js +12 -36
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/render/getRenderData.js +4 -4
- package/dist/render/getRenderData.js.map +1 -1
- package/dist/style.css +114 -163
- package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +1 -1
- package/dist/transcoding/utils/UrlGenerator.js +10 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -0
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +23 -1
- package/dist/utils/frameTime.js.map +1 -1
- package/package.json +45 -8
- package/scripts/build-css.js +8 -1
- package/test/setup.ts +0 -1
- package/test/useAssetMSW.ts +50 -0
- package/test/visualRegressionUtils.ts +23 -9
- package/tsdown.config.ts +6 -1
- package/dist/_virtual/rolldown_runtime.js +0 -27
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
- package/dist/elements/EFThumbnailStrip.d.ts +0 -167
- package/dist/elements/EFThumbnailStrip.js +0 -731
- package/dist/elements/EFThumbnailStrip.js.map +0 -1
- package/dist/elements/SessionThumbnailCache.js +0 -154
- package/dist/elements/SessionThumbnailCache.js.map +0 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
- package/dist/node_modules/react/cjs/react.development.js +0 -1521
- package/dist/node_modules/react/cjs/react.development.js.map +0 -1
- package/dist/node_modules/react/index.js +0 -13
- package/dist/node_modules/react/index.js.map +0 -1
- package/dist/node_modules/react/jsx-runtime.js +0 -13
- package/dist/node_modules/react/jsx-runtime.js.map +0 -1
- package/dist/preview/encoding/types.d.ts +0 -1
- package/dist/preview/renderTimegroupPreview.js +0 -686
- package/dist/preview/renderTimegroupPreview.js.map +0 -1
- package/dist/preview/rendering/renderToImage.d.ts +0 -2
- package/dist/preview/rendering/renderToImage.js +0 -95
- package/dist/preview/rendering/renderToImage.js.map +0 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
- package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
- package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
- package/dist/preview/rendering/svgSerializer.js +0 -43
- package/dist/preview/rendering/svgSerializer.js.map +0 -1
- package/dist/preview/rendering/types.d.ts +0 -2
- package/dist/preview/thumbnailCacheSettings.js +0 -52
- package/dist/preview/thumbnailCacheSettings.js.map +0 -1
- package/dist/sandbox/PlaybackControls.d.ts +0 -1
- package/dist/sandbox/PlaybackControls.js +0 -10
- package/dist/sandbox/PlaybackControls.js.map +0 -1
- package/dist/sandbox/ScenarioRunner.d.ts +0 -1
- package/dist/sandbox/ScenarioRunner.js +0 -1
- package/dist/sandbox/defineSandbox.d.ts +0 -1
- package/dist/sandbox/index.d.ts +0 -3
- package/dist/sandbox/index.js +0 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -80
- package/test/thumbnail-performance-test.html +0 -116
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetMediaEngine.js","names":["#cachedVideoRendition","#cachedAudioRendition","segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n // MediaEngine interface properties\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia, \n urlGenerator: UrlGenerator, \n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n \n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\")\n ? src.slice(1)\n : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n \n // Use production API format: /api/v1/isobmff_files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const apiBaseUrl = urlGenerator.getBaseUrl();\n const url = apiBaseUrl \n ? `${apiBaseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Initialize MediaEngine interface properties\n const sourceUrl = engine.getSourceUrlForJit();\n const jitBaseUrl = engine.getBaseUrlForJit();\n engine.templates = {\n initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.getVideoTrackIndex();\n const audioTrack = engine.getAudioTrackIndex();\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\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 // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\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 // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n getAudioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n getVideoTrackIndex() {\n return Object.values(this.data).find(\n (track) => track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n getScrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n // Cache renditions to avoid recomputing on every access\n #cachedVideoRendition: VideoRendition | undefined | null = null;\n #cachedAudioRendition: AudioRendition | undefined | null = null;\n\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n const videoTrack = this.getVideoTrackIndex();\n\n if (!videoTrack || videoTrack.track === undefined) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n const audioTrack = this.getAudioTrackIndex();\n\n if (!audioTrack || audioTrack.track === undefined) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n return this.#cachedAudioRendition;\n }\n\n // MediaEngine interface properties\n get videoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n get audioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n \n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n \n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\"init\", renditionId, this);\n \n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.getScrubTrackIndex();\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.getVideoRenditionInternal();\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n \n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetMediaEngine.js","names":["#cachedVideoRendition","#cachedAudioRendition","segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n // MediaEngine interface properties\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia,\n urlGenerator: UrlGenerator,\n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n\n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\") ? src.slice(1) : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n\n // Use production API format: /api/v1/files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const apiBaseUrl = urlGenerator.getBaseUrl();\n const url = apiBaseUrl\n ? `${apiBaseUrl}/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Initialize MediaEngine interface properties\n const sourceUrl = engine.getSourceUrlForJit();\n const jitBaseUrl = engine.getBaseUrlForJit();\n engine.templates = {\n initSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${jitBaseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.getVideoTrackIndex();\n const audioTrack = engine.getAudioTrackIndex();\n const needsVideo =\n requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio =\n requiredTracks === \"audio\" || requiredTracks === \"both\";\n\n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\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 // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `Video segments require authentication: ${error.message}`,\n );\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n\n // Check for abort between validations\n signal?.throwIfAborted();\n\n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\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 // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") &&\n error.message.includes(\"401\")))\n ) {\n throw new Error(\n `Audio segments require authentication: ${error.message}`,\n );\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n getAudioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n getVideoTrackIndex() {\n return Object.values(this.data).find(\n (track) =>\n track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n getScrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n // Cache renditions to avoid recomputing on every access\n #cachedVideoRendition: VideoRendition | undefined | null = null;\n #cachedAudioRendition: AudioRendition | undefined | null = null;\n\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n const videoTrack = this.getVideoTrackIndex();\n\n if (!videoTrack || videoTrack.track === undefined) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n const audioTrack = this.getAudioTrackIndex();\n\n if (!audioTrack || audioTrack.track === undefined) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n return this.#cachedAudioRendition;\n }\n\n // MediaEngine interface properties\n get videoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n get audioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n\n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n\n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n\n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(\n jitSegmentId,\n rendition.id,\n this,\n );\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n\n // Use rendition ID if provided, otherwise map from trackId\n const renditionId =\n rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\n \"init\",\n renditionId,\n this,\n );\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n\n // Use rendition ID if provided, otherwise map from trackId\n const renditionId =\n rendition.id || this.getRenditionId(rendition.trackId);\n\n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(\n jitSegmentId,\n renditionId,\n this,\n );\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.getScrubTrackIndex();\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.getVideoRenditionInternal();\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n\n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -24,12 +24,6 @@ var BaseMediaEngine = class {
|
|
|
24
24
|
return this.getAudioRenditionInternal();
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Generate cache key for segment requests
|
|
28
|
-
*/
|
|
29
|
-
getSegmentCacheKey(segmentId, rendition) {
|
|
30
|
-
return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
27
|
* Unified fetch method with caching and global deduplication
|
|
34
28
|
* All requests (media, manifest, init segments) go through this method
|
|
35
29
|
*/
|
|
@@ -69,14 +63,11 @@ var BaseMediaEngine = class {
|
|
|
69
63
|
});
|
|
70
64
|
const fetchEnd = performance.now();
|
|
71
65
|
span.setAttribute("fetchMs", fetchEnd - fetchStart);
|
|
72
|
-
|
|
73
|
-
const text = await response.text();
|
|
74
|
-
throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
|
|
75
|
-
}
|
|
66
|
+
const contentType = response.headers.get("content-type");
|
|
76
67
|
if (responseType === "json") {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
if (!response.ok || contentType && !contentType.includes("application/json") && !contentType.includes("text/json")) {
|
|
69
|
+
const text = await response.clone().text();
|
|
70
|
+
if (!response.ok) throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
|
|
80
71
|
throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
|
|
81
72
|
}
|
|
82
73
|
try {
|
|
@@ -85,6 +76,10 @@ var BaseMediaEngine = class {
|
|
|
85
76
|
throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
77
|
}
|
|
87
78
|
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.clone().text();
|
|
81
|
+
throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
|
|
82
|
+
}
|
|
88
83
|
const buffer = await response.arrayBuffer();
|
|
89
84
|
span.setAttribute("sizeBytes", buffer.byteLength);
|
|
90
85
|
return buffer;
|
|
@@ -116,11 +111,15 @@ var BaseMediaEngine = class {
|
|
|
116
111
|
*/
|
|
117
112
|
handleAbortForCachedRequest(promise, signal) {
|
|
118
113
|
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
119
|
-
|
|
114
|
+
const abortPromise = new Promise((_, reject) => {
|
|
120
115
|
signal.addEventListener("abort", () => {
|
|
121
116
|
reject(new DOMException("Aborted", "AbortError"));
|
|
122
117
|
});
|
|
123
|
-
})
|
|
118
|
+
});
|
|
119
|
+
abortPromise.catch(() => {});
|
|
120
|
+
const racePromise = Promise.race([promise, abortPromise]);
|
|
121
|
+
racePromise.catch(() => {});
|
|
122
|
+
return racePromise;
|
|
124
123
|
}
|
|
125
124
|
async fetchMedia(url, signal) {
|
|
126
125
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
@@ -144,44 +143,6 @@ var BaseMediaEngine = class {
|
|
|
144
143
|
signal
|
|
145
144
|
});
|
|
146
145
|
}
|
|
147
|
-
async fetchMediaCache(url, signal) {
|
|
148
|
-
return this.fetchMedia(url, signal);
|
|
149
|
-
}
|
|
150
|
-
async fetchManifestCache(url, signal) {
|
|
151
|
-
return this.fetchManifest(url, signal);
|
|
152
|
-
}
|
|
153
|
-
async fetchMediaCacheWithHeaders(url, headers, signal) {
|
|
154
|
-
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Fetch media segment with built-in deduplication
|
|
158
|
-
* Now uses global deduplication for all requests
|
|
159
|
-
*/
|
|
160
|
-
async fetchMediaSegmentWithDeduplication(segmentId, rendition, signal) {
|
|
161
|
-
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
162
|
-
return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
163
|
-
return this.fetchMediaSegment(segmentId, rendition, signal);
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Check if a segment is currently being fetched
|
|
168
|
-
*/
|
|
169
|
-
isSegmentBeingFetched(segmentId, rendition) {
|
|
170
|
-
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
171
|
-
return globalRequestDeduplicator.isPending(cacheKey);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Get count of active segment requests (for debugging/monitoring)
|
|
175
|
-
*/
|
|
176
|
-
getActiveSegmentRequestCount() {
|
|
177
|
-
return globalRequestDeduplicator.getPendingCount();
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Cancel all active segment requests (for cleanup)
|
|
181
|
-
*/
|
|
182
|
-
cancelAllSegmentRequests() {
|
|
183
|
-
globalRequestDeduplicator.clear();
|
|
184
|
-
}
|
|
185
146
|
/**
|
|
186
147
|
* Calculate audio segments needed for a time range
|
|
187
148
|
* Each media engine implements this based on their segment structure
|
|
@@ -224,52 +185,14 @@ var BaseMediaEngine = class {
|
|
|
224
185
|
return segments;
|
|
225
186
|
}
|
|
226
187
|
/**
|
|
227
|
-
* Check if a segment is cached for a given rendition
|
|
228
|
-
* This needs to check the URL-based cache since that's where segments are actually stored
|
|
229
|
-
*/
|
|
230
|
-
isSegmentCached(segmentId, rendition) {
|
|
231
|
-
try {
|
|
232
|
-
const maybeJitEngine = this;
|
|
233
|
-
if (maybeJitEngine.urlGenerator && typeof maybeJitEngine.urlGenerator.generateSegmentUrl === "function") {
|
|
234
|
-
if (!rendition.id) return false;
|
|
235
|
-
const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(segmentId, rendition.id, maybeJitEngine);
|
|
236
|
-
return mediaCache.has(segmentUrl);
|
|
237
|
-
}
|
|
238
|
-
const cacheKey = `${rendition.src}-${rendition.id || "default"}-${segmentId}-${rendition.trackId}`;
|
|
239
|
-
return mediaCache.has(cacheKey);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
console.warn(`🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`, error);
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Get cached segment IDs from a list for a given rendition
|
|
247
|
-
*/
|
|
248
|
-
getCachedSegments(segmentIds, rendition) {
|
|
249
|
-
return new Set(segmentIds.filter((id) => this.isSegmentCached(id, rendition)));
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
188
|
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
253
189
|
* Default implementation provides helpful error information
|
|
254
190
|
*/
|
|
255
|
-
async extractThumbnails(timestamps,
|
|
191
|
+
async extractThumbnails(timestamps, _signal) {
|
|
256
192
|
const engineName = this.constructor.name;
|
|
257
193
|
console.warn(`${engineName}: extractThumbnails not properly implemented. This MediaEngine type does not support thumbnail generation. Supported engines: JitMediaEngine. Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? "" : "s"}.`);
|
|
258
194
|
return timestamps.map(() => null);
|
|
259
195
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Get buffer configuration for this media engine
|
|
262
|
-
* Can be overridden by subclasses to provide custom buffer settings
|
|
263
|
-
*/
|
|
264
|
-
getBufferConfig() {
|
|
265
|
-
return {
|
|
266
|
-
videoBufferDurationMs: 1e4,
|
|
267
|
-
audioBufferDurationMs: 1e4,
|
|
268
|
-
maxVideoBufferFetches: 3,
|
|
269
|
-
maxAudioBufferFetches: 3,
|
|
270
|
-
bufferThresholdMs: 3e4
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
196
|
};
|
|
274
197
|
|
|
275
198
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n // Use protected abstract methods instead of abstract getters to avoid TypeScript bug\n // See: https://github.com/microsoft/TypeScript/issues/58020\n // Note: Abstract getters ALSO trigger this bug, not just getters in object literals\n protected abstract getVideoRenditionInternal(): VideoRendition | undefined;\n protected abstract getAudioRenditionInternal(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined appropriately.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Generate cache key for segment requests\n */\n private getSegmentCacheKey(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined; id?: string },\n ): string {\n return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n // Pass the signal to host.fetch() so network requests can be canceled when tasks are aborted\n // If multiple callers are waiting on the same request and one aborts, the request will be canceled\n // Other callers will get an error, but they can retry if needed\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check response status before parsing\n if (!response.ok) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n\n if (responseType === \"json\") {\n // Check content type header (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\") && !contentType.includes(\"text/json\")) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n try {\n // Read body once as JSON\n return await response.json();\n } catch (error) {\n // JSON parse failed - body is already consumed, can't read again\n // The error should contain enough info, but if we need the text, we'd need to clone the response first\n throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Handle the case where the promise might be aborted\n promise.catch((error) => {\n // If the request was aborted, remove it from cache to prevent corrupted data\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n }),\n ]);\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n // Legacy methods for backward compatibility\n async fetchMediaCache(\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMedia(url, signal);\n }\n\n async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {\n return this.fetchManifest(url, signal);\n }\n\n async fetchMediaCacheWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMediaWithHeaders(url, headers, signal);\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Fetch media segment with built-in deduplication\n * Now uses global deduplication for all requests\n */\n async fetchMediaSegmentWithDeduplication(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n\n return globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n return this.fetchMediaSegment(segmentId, rendition, signal);\n });\n }\n\n /**\n * Check if a segment is currently being fetched\n */\n isSegmentBeingFetched(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined },\n ): boolean {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n return globalRequestDeduplicator.isPending(cacheKey);\n }\n\n /**\n * Get count of active segment requests (for debugging/monitoring)\n */\n getActiveSegmentRequestCount(): number {\n return globalRequestDeduplicator.getPendingCount();\n }\n\n /**\n * Cancel all active segment requests (for cleanup)\n */\n cancelAllSegmentRequests(): void {\n globalRequestDeduplicator.clear();\n }\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * This needs to check the URL-based cache since that's where segments are actually stored\n */\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n try {\n // Check if this is a JIT engine by looking for urlGenerator property\n const maybeJitEngine = this as any;\n if (\n maybeJitEngine.urlGenerator &&\n typeof maybeJitEngine.urlGenerator.generateSegmentUrl === \"function\"\n ) {\n // This is a JIT engine - generate the URL and check URL-based cache\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n maybeJitEngine,\n );\n const urlIsCached = mediaCache.has(segmentUrl);\n\n return urlIsCached;\n }\n // For other engine types, fall back to the old segment-based key approach\n const cacheKey = `${rendition.src}-${rendition.id || \"default\"}-${segmentId}-${rendition.trackId}`;\n const isCached = mediaCache.has(cacheKey);\n return isCached;\n } catch (error) {\n console.warn(\n `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,\n error,\n );\n return false;\n }\n }\n\n /**\n * Get cached segment IDs from a list for a given rendition\n */\n getCachedSegments(\n segmentIds: number[],\n rendition: AudioRendition | VideoRendition,\n ): Set<number> {\n return new Set(\n segmentIds.filter((id) => this.isSegmentCached(id, rendition)),\n );\n }\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n\n /**\n * Get buffer configuration for this media engine\n * Can be overridden by subclasses to provide custom buffer settings\n */\n getBufferConfig(): {\n videoBufferDurationMs: number;\n audioBufferDurationMs: number;\n maxVideoBufferFetches: number;\n maxAudioBufferFetches: number;\n bufferThresholdMs: number;\n } {\n return {\n videoBufferDurationMs: 10000, // 10 seconds\n audioBufferDurationMs: 10000, // 10 seconds\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n // Use protected abstract methods instead of abstract getters to avoid TypeScript bug\n // See: https://github.com/microsoft/TypeScript/issues/58020\n // Note: Abstract getters ALSO trigger this bug, not just getters in object literals\n protected abstract getVideoRenditionInternal(): VideoRendition | undefined;\n protected abstract getAudioRenditionInternal(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.getVideoRenditionInternal();\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined appropriately.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.getAudioRenditionInternal();\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check headers first (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n\n // For JSON responses, check both status and content type before consuming body\n if (responseType === \"json\") {\n // If response is not ok or content type is wrong, clone to read body for error message\n if (\n !response.ok ||\n (contentType &&\n !contentType.includes(\"application/json\") &&\n !contentType.includes(\"text/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(\n `Failed to fetch: ${response.status} ${text.substring(0, 100)}`,\n );\n }\n throw new Error(\n `Expected JSON but got ${contentType}: ${text.substring(0, 100)}`,\n );\n }\n\n // Response is ok and content type is correct, parse as JSON\n try {\n return await response.json();\n } catch (error) {\n // Body already consumed, can't read again for error details\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n // For arrayBuffer responses, check status before consuming body\n if (!response.ok) {\n const text = await response.clone().text();\n throw new Error(\n `Failed to fetch: ${response.status} ${text.substring(0, 100)}`,\n );\n }\n\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Suppress unhandled rejection on the cached promise — errors still propagate\n // to awaiters. Without this, a rejection while the promise sits in cache (with\n // no active awaiter) registers as an unhandled rejection in the browser/runtime.\n promise.catch((error) => {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n // All other errors are intentionally swallowed here; they will be thrown\n // again when the caller awaits fetchWithCache (lines below).\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request.\n // The abort promise must have .catch(() => {}) to prevent unhandled rejections\n // when the main promise resolves first and the abort fires later during cleanup.\n const abortPromise = new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n });\n abortPromise.catch(() => {});\n\n const racePromise = Promise.race([promise, abortPromise]);\n racePromise.catch(() => {});\n return racePromise;\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * Each engine implements its own cache key strategy\n */\n abstract isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean;\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n _signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n}\n"],"mappings":";;;;;AAaA,MAAa,aAAa,IAAI,kBAA0B,MAAM,OAAO,KAAK;AAC1E,MAAa,4BAA4B,IAAI,qBAAqB;AAElE,IAAsB,kBAAtB,MAAsC;CAGpC,YAAY,MAAe;AACzB,OAAK,OAAO;;;;;;CAad,oBAAgD;AAC9C,SAAO,KAAK,2BAA2B;;;;;;CAOzC,oBAAgD;AAC9C,SAAO,KAAK,2BAA2B;;;;;;CAOzC,MAAgB,eACd,KACA,SAKc;AACd,SAAO,SACL,8BACA;GACE,KAAK,IAAI,SAAS,MAAM,GAAG,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO;GACxD,cAAc,QAAQ;GACtB,YAAY,CAAC,CAAC,QAAQ;GACvB,EACD,QACA,OAAO,SAAS;GACd,MAAM,KAAK,YAAY,KAAK;GAC5B,MAAM,EAAE,cAAc,SAAS,WAAW;GAI1C,MAAM,WAAW,UAAU,GAAG,IAAI,GAAG,KAAK,UAAU,QAAQ,KAAK;GAGjE,MAAM,KAAK,YAAY,KAAK;GAC5B,MAAM,SAAS,WAAW,IAAI,SAAS;GACvC,MAAM,KAAK,YAAY,KAAK;AAC5B,QAAK,aAAa,iBAAiB,KAAK,OAAO,KAAK,MAAM,IAAK,GAAG,IAAK;AAEvE,OAAI,QAAQ;AACV,SAAK,aAAa,YAAY,KAAK;AAGnC,QAAI,QAAQ;KACV,MAAM,KAAK,YAAY,KAAK;KAC5B,MAAMA,WAAS,MAAM,KAAK,4BACxB,QACA,OACD;KACD,MAAM,KAAK,YAAY,KAAK;AAC5B,UAAK,aACH,iBACA,KAAK,OAAO,KAAK,MAAM,IAAI,GAAG,IAC/B;AACD,UAAK,aACH,mBACA,KAAK,OAAO,KAAK,MAAM,IAAI,GAAG,IAC/B;AACD,YAAOA;;AAET,SAAK,aACH,mBACA,KAAK,OAAO,KAAK,MAAM,IAAI,GAAG,IAC/B;AACD,WAAO;;AAGT,QAAK,aAAa,YAAY,MAAM;GAIpC,MAAM,UAAU,0BAA0B,eACxC,UACA,YAAY;IACV,MAAM,aAAa,YAAY,KAAK;AACpC,QAAI;KACF,MAAM,WAAW,MAAM,KAAK,KAAK,MAAM,KAAK;MAAE;MAAS;MAAQ,CAAC;KAChE,MAAM,WAAW,YAAY,KAAK;AAClC,UAAK,aAAa,WAAW,WAAW,WAAW;KAGnD,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe;AAGxD,SAAI,iBAAiB,QAAQ;AAE3B,UACE,CAAC,SAAS,MACT,eACC,CAAC,YAAY,SAAS,mBAAmB,IACzC,CAAC,YAAY,SAAS,YAAY,EACpC;OACA,MAAM,OAAO,MAAM,SAAS,OAAO,CAAC,MAAM;AAC1C,WAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MACR,oBAAoB,SAAS,OAAO,GAAG,KAAK,UAAU,GAAG,IAAI,GAC9D;AAEH,aAAM,IAAI,MACR,yBAAyB,YAAY,IAAI,KAAK,UAAU,GAAG,IAAI,GAChE;;AAIH,UAAI;AACF,cAAO,MAAM,SAAS,MAAM;eACrB,OAAO;AAEd,aAAM,IAAI,MACR,kCAAkC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GACzF;;;AAKL,SAAI,CAAC,SAAS,IAAI;MAChB,MAAM,OAAO,MAAM,SAAS,OAAO,CAAC,MAAM;AAC1C,YAAM,IAAI,MACR,oBAAoB,SAAS,OAAO,GAAG,KAAK,UAAU,GAAG,IAAI,GAC9D;;KAGH,MAAM,SAAS,MAAM,SAAS,aAAa;AAC3C,UAAK,aAAa,aAAa,OAAO,WAAW;AACjD,YAAO;aACA,OAAO;AAEd,SACE,iBAAiB,gBACjB,MAAM,SAAS,aAGf,YAAW,OAAO,SAAS;AAE7B,WAAM;;KAGX;AAGD,cAAW,IAAI,UAAU,QAAQ;AAKjC,WAAQ,OAAO,UAAU;AACvB,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,YAAW,OAAO,SAAS;KAI7B;AAGF,OAAI,QAAQ;IACV,MAAMA,WAAS,MAAM,KAAK,4BACxB,SACA,OACD;IACD,MAAMC,SAAO,YAAY,KAAK;AAC9B,SAAK,aACH,gBACA,KAAK,OAAOA,SAAO,MAAM,IAAI,GAAG,IACjC;AACD,WAAOD;;GAGT,MAAM,SAAS,MAAM;GACrB,MAAM,OAAO,YAAY,KAAK;AAC9B,QAAK,aAAa,gBAAgB,KAAK,OAAO,OAAO,MAAM,IAAI,GAAG,IAAI;AACtE,UAAO;IAEV;;;;;;;CAQH,AAAQ,4BACN,SACA,QACY;AAEZ,MAAI,OAAO,QACT,OAAM,IAAI,aAAa,WAAW,aAAa;EAOjD,MAAM,eAAe,IAAI,SAAgB,GAAG,WAAW;AACrD,UAAO,iBAAiB,eAAe;AACrC,WAAO,IAAI,aAAa,WAAW,aAAa,CAAC;KACjD;IACF;AACF,eAAa,YAAY,GAAG;EAE5B,MAAM,cAAc,QAAQ,KAAK,CAAC,SAAS,aAAa,CAAC;AACzD,cAAY,YAAY,GAAG;AAC3B,SAAO;;CAIT,MAAM,WAAW,KAAa,QAA4C;AAExE,MAAI,QAAQ,QACV,OAAM,IAAI,aAAa,WAAW,aAAa;AAEjD,SAAO,KAAK,eAAe,KAAK;GAAE,cAAc;GAAe;GAAQ,CAAC;;CAG1E,MAAM,cAAc,KAAa,QAAoC;AAEnE,MAAI,QAAQ,QACV,OAAM,IAAI,aAAa,WAAW,aAAa;AAEjD,SAAO,KAAK,eAAe,KAAK;GAAE,cAAc;GAAQ;GAAQ,CAAC;;CAGnE,MAAM,sBACJ,KACA,SACA,QACsB;AAEtB,MAAI,QAAQ,QACV,OAAM,IAAI,aAAa,WAAW,aAAa;AAEjD,SAAO,KAAK,eAAe,KAAK;GAC9B,cAAc;GACd;GACA;GACD,CAAC;;;;;;CA0BJ,2BACE,QACA,MACA,WACA,YACoB;AAEpB,MAAI,UAAU,KACZ,QAAO,EAAE;EAGX,MAAME,WAA+B,EAAE;AAGvC,MACE,UAAU,sBACV,UAAU,mBAAmB,SAAS,GACtC;GACA,IAAI,iBAAiB;AAErB,QAAK,IAAI,IAAI,GAAG,IAAI,UAAU,mBAAmB,QAAQ,KAAK;IAC5D,MAAM,kBAAkB,UAAU,mBAAmB;AACrD,QAAI,oBAAoB,OACtB;IAEF,MAAM,iBAAiB;IACvB,MAAM,eAAe,KAAK,IACxB,iBAAiB,iBACjB,WACD;AAGD,QAAI,kBAAkB,WACpB;AAIF,QAAI,iBAAiB,QAAQ,eAAe,OAC1C,UAAS,KAAK;KACZ,WAAW,IAAI;KACf,SAAS;KACT,OAAO;KACR,CAAC;AAGJ,sBAAkB;AAGlB,QAAI,kBAAkB,WACpB;;AAIJ,UAAO;;EAIT,MAAM,oBAAoB,UAAU,qBAAqB;EACzD,MAAM,oBAAoB,KAAK,MAAM,SAAS,kBAAkB;EAChE,MAAM,kBAAkB,KAAK,MAAM,OAAO,kBAAkB;AAE5D,OAAK,IAAI,IAAI,mBAAmB,KAAK,iBAAiB,KAAK;GACzD,MAAM,YAAY,IAAI;GACtB,MAAM,iBAAiB,IAAI;GAC3B,MAAM,eAAe,KAAK,KAAK,IAAI,KAAK,mBAAmB,WAAW;AAGtE,OAAI,kBAAkB,WACpB;AAIF,OAAI,iBAAiB,QAAQ,eAAe,OAC1C,UAAS,KAAK;IACZ;IACA,SAAS;IACT,OAAO;IACR,CAAC;;AAIN,SAAO;;;;;;CAgBT,MAAM,kBACJ,YACA,SACqC;EACrC,MAAM,aAAa,KAAK,YAAY;AACpC,UAAQ,KACN,GAAG,WAAW,0JAGC,WAAW,OAAO,YAAY,WAAW,WAAW,IAAI,KAAK,IAAI,GACjF;AACD,SAAO,WAAW,UAAU,KAAK"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
2
|
+
import { DEFAULT_MEDIABUNNY_TIMEOUT_MS, withTimeout } from "./shared/timeoutUtils.js";
|
|
2
3
|
import { roundToMilliseconds } from "./shared/PrecisionUtils.js";
|
|
3
4
|
import { SampleBuffer } from "../SampleBuffer.js";
|
|
4
5
|
import { AudioSampleSink, BufferSource, Input, InputAudioTrack, InputVideoTrack, MP4, VideoSampleSink } from "mediabunny";
|
|
@@ -45,25 +46,25 @@ var BufferedSeekingInput = class {
|
|
|
45
46
|
return this.input.computeDuration();
|
|
46
47
|
}
|
|
47
48
|
async getTrack(trackId) {
|
|
48
|
-
const track = (await this.input.getTracks()).find((track$1) => track$1.id === trackId);
|
|
49
|
+
const track = (await withTimeout(this.input.getTracks(), 5e3, "BufferedSeekingInput.getTracks")).find((track$1) => track$1.id === trackId);
|
|
49
50
|
if (!track) throw new Error(`Track ${trackId} not found`);
|
|
50
51
|
return track;
|
|
51
52
|
}
|
|
52
53
|
async getAudioTrack(trackId) {
|
|
53
|
-
const track = (await this.input.getAudioTracks()).find((track$1) => track$1.id === trackId && track$1.type === "audio");
|
|
54
|
+
const track = (await withTimeout(this.input.getAudioTracks(), 5e3, "BufferedSeekingInput.getAudioTracks")).find((track$1) => track$1.id === trackId && track$1.type === "audio");
|
|
54
55
|
if (!track) throw new Error(`Track ${trackId} not found`);
|
|
55
56
|
return track;
|
|
56
57
|
}
|
|
57
58
|
async getVideoTrack(trackId) {
|
|
58
|
-
const track = (await this.input.getVideoTracks()).find((track$1) => track$1.id === trackId && track$1.type === "video");
|
|
59
|
+
const track = (await withTimeout(this.input.getVideoTracks(), 5e3, "BufferedSeekingInput.getVideoTracks")).find((track$1) => track$1.id === trackId && track$1.type === "video");
|
|
59
60
|
if (!track) throw new Error(`Track ${trackId} not found`);
|
|
60
61
|
return track;
|
|
61
62
|
}
|
|
62
63
|
async getFirstVideoTrack() {
|
|
63
|
-
return (await this.input.getVideoTracks())[0];
|
|
64
|
+
return (await withTimeout(this.input.getVideoTracks(), 5e3, "BufferedSeekingInput.getFirstVideoTrack"))[0];
|
|
64
65
|
}
|
|
65
66
|
async getFirstAudioTrack() {
|
|
66
|
-
return (await this.input.getAudioTracks())[0];
|
|
67
|
+
return (await withTimeout(this.input.getAudioTracks(), 5e3, "BufferedSeekingInput.getFirstAudioTrack"))[0];
|
|
67
68
|
}
|
|
68
69
|
getTrackIterator(track) {
|
|
69
70
|
if (this.trackIterators.has(track.id)) return this.trackIterators.get(track.id);
|
|
@@ -143,13 +144,11 @@ var BufferedSeekingInput = class {
|
|
|
143
144
|
span.setAttribute("trackType", track.type);
|
|
144
145
|
const trackBuffer = this.getTrackBuffer(track);
|
|
145
146
|
const roundedTimeMs = roundToMilliseconds(timeMs);
|
|
146
|
-
const
|
|
147
|
+
const timeoutMs = 5e3;
|
|
148
|
+
const firstTimestampMs = roundToMilliseconds(await Promise.race([track.getFirstTimestamp(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`getFirstTimestamp timeout after ${timeoutMs}ms`)), timeoutMs))]) * 1e3);
|
|
147
149
|
span.setAttribute("firstTimestampMs", firstTimestampMs);
|
|
148
150
|
if (roundedTimeMs < firstTimestampMs - .01) {
|
|
149
|
-
console.error(
|
|
150
|
-
roundedTimeMs,
|
|
151
|
-
firstTimestampMs
|
|
152
|
-
});
|
|
151
|
+
console.error(`[BufferedSeekingInput.seekSafe] OUT_OF_BOUNDS trackId=${trackId} roundedTimeMs=${roundedTimeMs} firstTimestampMs=${firstTimestampMs}`);
|
|
153
152
|
throw new NoSample(`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`);
|
|
154
153
|
}
|
|
155
154
|
const bufferContents = trackBuffer.getContents();
|
|
@@ -191,7 +190,7 @@ var BufferedSeekingInput = class {
|
|
|
191
190
|
while (true) {
|
|
192
191
|
iterationCount++;
|
|
193
192
|
const iterStart = performance.now();
|
|
194
|
-
const { done, value: decodedSample } = await iterator.next();
|
|
193
|
+
const { done, value: decodedSample } = await withTimeout(iterator.next(), DEFAULT_MEDIABUNNY_TIMEOUT_MS, `iterator.next() for ${track.type} track ${trackId} iteration ${iterationCount}`);
|
|
195
194
|
const iterEnd = performance.now();
|
|
196
195
|
if (iterationCount <= 5) span.setAttribute(`iter${iterationCount}Ms`, Math.round((iterEnd - iterStart) * 100) / 100);
|
|
197
196
|
if (decodedSample) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await this.input.getTracks();\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await this.input.getAudioTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"audio\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await this.input.getVideoTracks();\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"video\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await this.input.getVideoTracks();\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await this.input.getAudioTracks();\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n return await seekPromise;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(\n track.id,\n );\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n const firstTimestampMs = roundToMilliseconds(\n (await track.getFirstTimestamp()) * 1000,\n );\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n // Use tolerance for floating point comparison (0.01ms tolerance)\n // This handles rounding errors like 20916.666 vs 20916.667\n const PRECISION_TOLERANCE_MS = 0.01;\n if (roundedTimeMs < firstTimestampMs - PRECISION_TOLERANCE_MS) {\n console.error(\"Seeking outside bounds of input\", {\n roundedTimeMs,\n firstTimestampMs,\n });\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(\n trackBuffer.firstTimestamp * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round(\n (firstSample.timestamp || 0) * 1000,\n );\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *\n 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\n \"bufferRangeMs\",\n `${bufferStartMs}-${bufferEndMs}`,\n );\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await iterator.next();\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\n \"decodeMs\",\n Math.round((decodeEnd - decodeStart) * 100) / 100,\n );\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /\n 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\n \"foundTimestamp\",\n Math.round((foundSample.timestamp || 0) * 1000),\n );\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample =\n finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *\n 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) ===\n roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack =\n roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BufferedSeekingInput.js","names":["defaultOptions: BufferedSeekingInputOptions","track","bufferSize","#seekLock","contents"],"sources":["../../../src/elements/EFMedia/BufferedSeekingInput.ts"],"sourcesContent":["import {\n AudioSampleSink,\n BufferSource,\n Input,\n InputAudioTrack,\n type InputTrack,\n InputVideoTrack,\n MP4,\n VideoSampleSink,\n} from \"mediabunny\";\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { type MediaSample, SampleBuffer } from \"../SampleBuffer\";\nimport { roundToMilliseconds } from \"./shared/PrecisionUtils\";\nimport {\n withTimeout,\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n} from \"./shared/timeoutUtils\";\n\ninterface BufferedSeekingInputOptions {\n videoBufferSize?: number;\n audioBufferSize?: number;\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n startTimeOffsetMs?: number;\n}\n\nconst defaultOptions: BufferedSeekingInputOptions = {\n videoBufferSize: 30,\n audioBufferSize: 100,\n startTimeOffsetMs: 0,\n};\n\nexport class NoSample extends RangeError {}\n\nexport class ConcurrentSeekError extends RangeError {}\n\nexport class BufferedSeekingInput {\n private input: Input;\n private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();\n private trackBuffers: Map<number, SampleBuffer> = new Map();\n private options: BufferedSeekingInputOptions;\n // Separate locks for different operation types to prevent unnecessary blocking\n private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();\n private trackSeekPromises: Map<number, Promise<any>> = new Map();\n\n /**\n * Timeline offset in milliseconds to map user timeline to media timeline.\n * Applied during seeking to handle media that doesn't start at 0ms.\n */\n private readonly startTimeOffsetMs: number;\n\n constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {\n const bufferSource = new BufferSource(arrayBuffer);\n const input = new Input({\n source: bufferSource,\n formats: [MP4],\n });\n this.input = input;\n this.options = { ...defaultOptions, ...options };\n this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;\n }\n\n // Buffer inspection API for testing\n getBufferSize(trackId: number): number {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? buffer.length : 0;\n }\n\n getBufferContents(trackId: number): readonly MediaSample[] {\n const buffer = this.trackBuffers.get(trackId);\n return buffer ? Object.freeze([...buffer.getContents()]) : [];\n }\n\n getBufferTimestamps(trackId: number): number[] {\n const contents = this.getBufferContents(trackId);\n return contents.map((sample) => sample.timestamp || 0);\n }\n\n clearBuffer(trackId: number): void {\n const buffer = this.trackBuffers.get(trackId);\n if (buffer) {\n buffer.clear();\n }\n }\n\n computeDuration() {\n return this.input.computeDuration();\n }\n\n async getTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getTracks(),\n 5000,\n \"BufferedSeekingInput.getTracks\",\n );\n const track = tracks.find((track) => track.id === trackId);\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getAudioTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getAudioTracks\",\n );\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"audio\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getVideoTrack(trackId: number) {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getVideoTracks\",\n );\n const track = tracks.find(\n (track) => track.id === trackId && track.type === \"video\",\n );\n if (!track) {\n throw new Error(`Track ${trackId} not found`);\n }\n return track;\n }\n\n async getFirstVideoTrack() {\n const tracks = await withTimeout(\n this.input.getVideoTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstVideoTrack\",\n );\n return tracks[0];\n }\n\n async getFirstAudioTrack() {\n const tracks = await withTimeout(\n this.input.getAudioTracks(),\n 5000,\n \"BufferedSeekingInput.getFirstAudioTrack\",\n );\n return tracks[0];\n }\n\n getTrackIterator(track: InputTrack) {\n if (this.trackIterators.has(track.id)) {\n // biome-ignore lint/style/noNonNullAssertion: we know the map has the key\n return this.trackIterators.get(track.id)!;\n }\n\n const trackIterator = this.createTrackIterator(track);\n\n this.trackIterators.set(track.id, trackIterator);\n\n return trackIterator;\n }\n\n createTrackSampleSink(track: InputTrack) {\n if (track instanceof InputAudioTrack) {\n return new AudioSampleSink(track);\n }\n if (track instanceof InputVideoTrack) {\n return new VideoSampleSink(track);\n }\n throw new Error(`Unsupported track type ${track.type}`);\n }\n\n createTrackIterator(track: InputTrack) {\n const sampleSink = this.createTrackSampleSink(track);\n return sampleSink.samples();\n }\n\n createTrackBuffer(track: InputTrack) {\n if (track.type === \"audio\") {\n const bufferSize = this.options.audioBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n const bufferSize = this.options.videoBufferSize;\n const sampleBuffer = new SampleBuffer(bufferSize);\n return sampleBuffer;\n }\n\n getTrackBuffer(track: InputTrack) {\n const maybeTrackBuffer = this.trackBuffers.get(track.id);\n\n if (maybeTrackBuffer) {\n return maybeTrackBuffer;\n }\n\n const trackBuffer = this.createTrackBuffer(track);\n this.trackBuffers.set(track.id, trackBuffer);\n return trackBuffer;\n }\n\n async seek(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seek\",\n {\n trackId,\n timeMs,\n startTimeOffsetMs: this.startTimeOffsetMs,\n },\n undefined,\n async (span) => {\n // Apply timeline offset to map user timeline to media timeline\n const mediaTimeMs = timeMs + this.startTimeOffsetMs;\n\n // Round using consistent precision handling\n const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);\n span.setAttribute(\"roundedMediaTimeMs\", roundedMediaTimeMs);\n\n // Serialize seek operations per track (but don't block iterator creation)\n const existingSeek = this.trackSeekPromises.get(trackId);\n if (existingSeek) {\n span.setAttribute(\"waitedForExistingSeek\", true);\n await existingSeek;\n }\n\n const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);\n this.trackSeekPromises.set(trackId, seekPromise);\n\n try {\n const result = await seekPromise;\n return result;\n } finally {\n this.trackSeekPromises.delete(trackId);\n }\n },\n );\n }\n\n private async resetIterator(track: InputTrack) {\n const trackBuffer = this.trackBuffers.get(track.id);\n trackBuffer?.clear();\n // Clean up iterator safely - wait for any ongoing iterator creation\n const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(\n track.id,\n );\n if (ongoingIteratorCreation) {\n await ongoingIteratorCreation;\n }\n\n const iterator = this.trackIterators.get(track.id);\n if (iterator) {\n try {\n await iterator.return?.();\n } catch (_error) {\n // Iterator cleanup failed, continue anyway\n }\n }\n this.trackIterators.delete(track.id);\n }\n\n #seekLock?: PromiseWithResolvers<void>;\n\n private async seekSafe(trackId: number, timeMs: number) {\n return withSpan(\n \"bufferedInput.seekSafe\",\n {\n trackId,\n timeMs,\n },\n undefined,\n async (span) => {\n if (this.#seekLock) {\n span.setAttribute(\"waitedForSeekLock\", true);\n await this.#seekLock.promise;\n }\n const seekLock = Promise.withResolvers<void>();\n this.#seekLock = seekLock;\n\n try {\n const track = await this.getTrack(trackId);\n span.setAttribute(\"trackType\", track.type);\n\n const trackBuffer = this.getTrackBuffer(track);\n\n const roundedTimeMs = roundToMilliseconds(timeMs);\n\n // Add timeout to detect if getFirstTimestamp hangs\n const timeoutMs = 5000;\n const firstTimestamp = await Promise.race([\n track.getFirstTimestamp(),\n new Promise<number>((_, reject) =>\n setTimeout(\n () =>\n reject(\n new Error(`getFirstTimestamp timeout after ${timeoutMs}ms`),\n ),\n timeoutMs,\n ),\n ),\n ]);\n const firstTimestampMs = roundToMilliseconds(firstTimestamp * 1000);\n\n span.setAttribute(\"firstTimestampMs\", firstTimestampMs);\n\n // Use tolerance for floating point comparison (0.01ms tolerance)\n // This handles rounding errors like 20916.666 vs 20916.667\n const PRECISION_TOLERANCE_MS = 0.01;\n if (roundedTimeMs < firstTimestampMs - PRECISION_TOLERANCE_MS) {\n console.error(\n `[BufferedSeekingInput.seekSafe] OUT_OF_BOUNDS trackId=${trackId} roundedTimeMs=${roundedTimeMs} firstTimestampMs=${firstTimestampMs}`,\n );\n throw new NoSample(\n `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,\n );\n }\n\n // Check if we need to reset iterator for seeks outside current buffer range\n const bufferContents = trackBuffer.getContents();\n span.setAttribute(\"bufferContentsLength\", bufferContents.length);\n\n if (bufferContents.length > 0) {\n const bufferStartMs = roundToMilliseconds(\n trackBuffer.firstTimestamp * 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n\n if (roundedTimeMs < bufferStartMs) {\n span.setAttribute(\"resetIterator\", true);\n await this.resetIterator(track);\n }\n }\n\n const alreadyInBuffer = trackBuffer.find(timeMs);\n if (alreadyInBuffer) {\n span.setAttribute(\"foundInBuffer\", true);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n span.setAttribute(\n \"bufferTimestamps\",\n contents\n .map((s) => Math.round((s.timestamp || 0) * 1000))\n .slice(0, 10)\n .join(\",\"),\n );\n }\n return alreadyInBuffer;\n }\n\n // Buffer miss - record buffer state\n span.setAttribute(\"foundInBuffer\", false);\n span.setAttribute(\"bufferSize\", trackBuffer.length);\n span.setAttribute(\"requestedTimeMs\", Math.round(timeMs));\n\n const contents = trackBuffer.getContents();\n if (contents.length > 0) {\n const firstSample = contents[0];\n const lastSample = contents[contents.length - 1];\n if (firstSample && lastSample) {\n const bufferStartMs = Math.round(\n (firstSample.timestamp || 0) * 1000,\n );\n const bufferEndMs = Math.round(\n ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *\n 1000,\n );\n span.setAttribute(\"bufferStartMs\", bufferStartMs);\n span.setAttribute(\"bufferEndMs\", bufferEndMs);\n span.setAttribute(\n \"bufferRangeMs\",\n `${bufferStartMs}-${bufferEndMs}`,\n );\n }\n }\n\n const iterator = this.getTrackIterator(track);\n let iterationCount = 0;\n const decodeStart = performance.now();\n\n while (true) {\n iterationCount++;\n const iterStart = performance.now();\n const { done, value: decodedSample } = await withTimeout(\n iterator.next(),\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n `iterator.next() for ${track.type} track ${trackId} iteration ${iterationCount}`,\n );\n const iterEnd = performance.now();\n\n // Record individual iteration timing for first 5 iterations\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Ms`,\n Math.round((iterEnd - iterStart) * 100) / 100,\n );\n }\n\n if (decodedSample) {\n trackBuffer.push(decodedSample);\n if (iterationCount <= 5) {\n span.setAttribute(\n `iter${iterationCount}Timestamp`,\n Math.round((decodedSample.timestamp || 0) * 1000),\n );\n }\n }\n\n const foundSample = trackBuffer.find(roundedTimeMs);\n if (foundSample) {\n const decodeEnd = performance.now();\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\n \"decodeMs\",\n Math.round((decodeEnd - decodeStart) * 100) / 100,\n );\n span.setAttribute(\n \"avgIterMs\",\n Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /\n 100,\n );\n span.setAttribute(\"foundSample\", true);\n span.setAttribute(\n \"foundTimestamp\",\n Math.round((foundSample.timestamp || 0) * 1000),\n );\n return foundSample;\n }\n if (done) {\n break;\n }\n }\n\n span.setAttribute(\"iterationCount\", iterationCount);\n span.setAttribute(\"reachedEnd\", true);\n\n // Check if we're seeking to the exact end of the track (legitimate use case)\n const finalBufferContents = trackBuffer.getContents();\n if (finalBufferContents.length > 0) {\n const lastSample =\n finalBufferContents[finalBufferContents.length - 1];\n const lastSampleEndMs = roundToMilliseconds(\n ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *\n 1000,\n );\n\n // Only return last sample if seeking to exactly the track duration\n // (end of video) AND we have the final segment loaded\n const trackDurationMs = (await track.computeDuration()) * 1000;\n const isSeekingToTrackEnd =\n roundToMilliseconds(timeMs) ===\n roundToMilliseconds(trackDurationMs);\n const isAtEndOfTrack =\n roundToMilliseconds(timeMs) >= lastSampleEndMs;\n\n if (isSeekingToTrackEnd && isAtEndOfTrack) {\n span.setAttribute(\"returnedLastSample\", true);\n return lastSample;\n }\n }\n\n // For all other cases (seeking within track but outside buffer range), throw error\n // The caller should ensure the correct segment is loaded before seeking\n throw new NoSample(\n `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,\n );\n } finally {\n this.#seekLock = undefined;\n seekLock.resolve();\n }\n },\n );\n }\n}\n"],"mappings":"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"}
|