@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,18 +1,15 @@
|
|
|
1
1
|
import { AssetMediaEngine } from "./AssetMediaEngine.js";
|
|
2
2
|
|
|
3
|
-
//#region src/elements/EFMedia/
|
|
4
|
-
var
|
|
5
|
-
static async
|
|
6
|
-
const url = `${apiHost}/api/v1/
|
|
3
|
+
//#region src/elements/EFMedia/FileMediaEngine.ts
|
|
4
|
+
var FileMediaEngine = class FileMediaEngine extends AssetMediaEngine {
|
|
5
|
+
static async fetchByFileId(host, _urlGenerator, fileId, apiHost, requiredTracks = "both", signal) {
|
|
6
|
+
const url = `${apiHost}/api/v1/files/${fileId}/index`;
|
|
7
7
|
const response = await host.fetch(url, { signal });
|
|
8
8
|
signal?.throwIfAborted();
|
|
9
|
-
if (!response.ok) {
|
|
10
|
-
const text = await response.text();
|
|
11
|
-
throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);
|
|
12
|
-
}
|
|
13
9
|
const contentType = response.headers.get("content-type");
|
|
14
|
-
if (contentType && !contentType.includes("application/json")) {
|
|
15
|
-
const text = await response.text();
|
|
10
|
+
if (!response.ok || contentType && !contentType.includes("application/json")) {
|
|
11
|
+
const text = await response.clone().text();
|
|
12
|
+
if (!response.ok) throw new Error(`Failed to fetch asset index: ${response.status} ${text}`);
|
|
16
13
|
throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
|
|
17
14
|
}
|
|
18
15
|
let data;
|
|
@@ -21,10 +18,9 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
|
|
|
21
18
|
signal?.throwIfAborted();
|
|
22
19
|
} catch (error) {
|
|
23
20
|
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
24
|
-
|
|
25
|
-
throw new Error(`Failed to parse JSON response: ${text.substring(0, 100)}`);
|
|
21
|
+
throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
|
|
26
22
|
}
|
|
27
|
-
const engine = new
|
|
23
|
+
const engine = new FileMediaEngine(host, fileId, data, apiHost, _urlGenerator);
|
|
28
24
|
signal?.throwIfAborted();
|
|
29
25
|
if (signal) {
|
|
30
26
|
const videoTrack = engine.getVideoTrackIndex();
|
|
@@ -53,38 +49,62 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
|
|
|
53
49
|
}
|
|
54
50
|
return engine;
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
static {
|
|
53
|
+
this.fetchByAssetId = FileMediaEngine.fetchByFileId;
|
|
54
|
+
}
|
|
55
|
+
constructor(host, fileId, data, apiHost, urlGenerator) {
|
|
56
|
+
super(host, fileId, urlGenerator);
|
|
59
57
|
this.apiHost = apiHost;
|
|
58
|
+
this.fileId = fileId;
|
|
60
59
|
this.data = data;
|
|
61
60
|
this.durationMs = Object.values(this.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
|
|
62
61
|
this.templates = {
|
|
63
|
-
initSegment: `${apiHost}/api/v1/
|
|
64
|
-
mediaSegment: `${apiHost}/api/v1/
|
|
62
|
+
initSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,
|
|
63
|
+
mediaSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`
|
|
65
64
|
};
|
|
66
65
|
}
|
|
66
|
+
/** @deprecated Use fileId instead */
|
|
67
|
+
get assetId() {
|
|
68
|
+
return this.fileId;
|
|
69
|
+
}
|
|
67
70
|
getInitSegmentPaths() {
|
|
68
71
|
const paths = {};
|
|
69
72
|
const audioTrack = this.getAudioTrackIndex();
|
|
70
73
|
const videoTrack = this.getVideoTrackIndex();
|
|
71
74
|
if (audioTrack !== void 0) paths.audio = {
|
|
72
|
-
path: `${this.apiHost}/api/v1/
|
|
75
|
+
path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${audioTrack.track}`,
|
|
73
76
|
pos: audioTrack.initSegment.offset,
|
|
74
77
|
size: audioTrack.initSegment.size
|
|
75
78
|
};
|
|
76
79
|
if (videoTrack !== void 0) paths.video = {
|
|
77
|
-
path: `${this.apiHost}/api/v1/
|
|
80
|
+
path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${videoTrack.track}`,
|
|
78
81
|
pos: videoTrack.initSegment.offset,
|
|
79
82
|
size: videoTrack.initSegment.size
|
|
80
83
|
};
|
|
81
84
|
return paths;
|
|
82
85
|
}
|
|
83
86
|
buildInitSegmentUrl(trackId) {
|
|
84
|
-
return `${this.apiHost}/api/v1/
|
|
87
|
+
return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;
|
|
85
88
|
}
|
|
86
89
|
buildMediaSegmentUrl(trackId, _segmentId) {
|
|
87
|
-
return `${this.apiHost}/api/v1/
|
|
90
|
+
return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;
|
|
91
|
+
}
|
|
92
|
+
async fetchInitSegment(rendition, signal) {
|
|
93
|
+
if (!rendition.trackId) throw new Error("[fetchInitSegment] Track ID is required for file-based media");
|
|
94
|
+
const trackData = this.data[rendition.trackId];
|
|
95
|
+
if (!trackData) throw new Error(`Track ${rendition.trackId} not found`);
|
|
96
|
+
const { offset, size } = trackData.initSegment;
|
|
97
|
+
const url = this.buildInitSegmentUrl(rendition.trackId);
|
|
98
|
+
return (await this.fetchMedia(url, signal)).slice(offset, offset + size);
|
|
99
|
+
}
|
|
100
|
+
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
101
|
+
if (!rendition.trackId) throw new Error("[fetchMediaSegment] Track ID is required for file-based media");
|
|
102
|
+
const trackData = this.data[rendition.trackId];
|
|
103
|
+
if (!trackData) throw new Error(`Track ${rendition.trackId} not found`);
|
|
104
|
+
const segment = trackData.segments[segmentId];
|
|
105
|
+
if (!segment) throw new Error(`Segment ${segmentId} not found for track ${rendition.trackId}`);
|
|
106
|
+
const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
|
|
107
|
+
return (await this.fetchMedia(url, signal)).slice(segment.offset, segment.offset + segment.size);
|
|
88
108
|
}
|
|
89
109
|
convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
|
|
90
110
|
if (!rendition.trackId) throw new Error("[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata");
|
|
@@ -98,5 +118,5 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
|
|
|
98
118
|
};
|
|
99
119
|
|
|
100
120
|
//#endregion
|
|
101
|
-
export {
|
|
102
|
-
//# sourceMappingURL=
|
|
121
|
+
export { FileMediaEngine };
|
|
122
|
+
//# sourceMappingURL=FileMediaEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileMediaEngine.js","names":["data: Record<number, TrackFragmentIndex>","apiHost: string","paths: InitSegmentPaths"],"sources":["../../../src/elements/EFMedia/FileMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\nimport type {\n InitSegmentPaths,\n MediaEngine,\n RenditionId,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { AssetMediaEngine } from \"./AssetMediaEngine\";\n\nexport class FileMediaEngine extends AssetMediaEngine implements MediaEngine {\n static async fetchByFileId(\n host: EFMedia,\n _urlGenerator: UrlGenerator,\n fileId: string,\n apiHost: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const url = `${apiHost}/api/v1/files/${fileId}/index`;\n const response = await host.fetch(url, { signal });\n\n signal?.throwIfAborted();\n\n const contentType = response.headers.get(\"content-type\");\n\n if (\n !response.ok ||\n (contentType && !contentType.includes(\"application/json\"))\n ) {\n const text = await response.clone().text();\n if (!response.ok) {\n throw new Error(\n `Failed to fetch asset index: ${response.status} ${text}`,\n );\n }\n throw new Error(\n `Expected JSON but got ${contentType}: ${text.substring(0, 100)}`,\n );\n }\n\n let data: Record<number, TrackFragmentIndex>;\n try {\n data = (await response.json()) as Record<number, TrackFragmentIndex>;\n signal?.throwIfAborted();\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new Error(\n `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n const engine = new FileMediaEngine(\n host,\n fileId,\n data,\n apiHost,\n _urlGenerator,\n );\n\n signal?.throwIfAborted();\n\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 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 (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\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 }\n }\n\n signal?.throwIfAborted();\n\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 (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\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 }\n }\n }\n\n return engine;\n }\n\n /** @deprecated Use fetchByFileId instead */\n static fetchByAssetId = FileMediaEngine.fetchByFileId;\n\n public fileId: string;\n\n constructor(\n host: EFMedia,\n fileId: string,\n data: Record<number, TrackFragmentIndex>,\n private apiHost: string,\n urlGenerator: UrlGenerator,\n ) {\n super(host, fileId, urlGenerator);\n this.fileId = fileId;\n this.data = data;\n\n const longestFragment = Object.values(this.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n this.durationMs = longestFragment * 1000;\n\n this.templates = {\n initSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,\n mediaSegment: `${apiHost}/api/v1/files/${fileId}/tracks/{trackId}`,\n };\n }\n\n /** @deprecated Use fileId instead */\n get assetId(): string {\n return this.fileId;\n }\n\n getInitSegmentPaths(): InitSegmentPaths {\n const paths: InitSegmentPaths = {};\n const audioTrack = this.getAudioTrackIndex();\n const videoTrack = this.getVideoTrackIndex();\n\n if (audioTrack !== undefined) {\n paths.audio = {\n path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${audioTrack.track}`,\n pos: audioTrack.initSegment.offset,\n size: audioTrack.initSegment.size,\n };\n }\n\n if (videoTrack !== undefined) {\n paths.video = {\n path: `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${videoTrack.track}`,\n pos: videoTrack.initSegment.offset,\n size: videoTrack.initSegment.size,\n };\n }\n\n return paths;\n }\n\n templates!: { initSegment: string; mediaSegment: string };\n\n buildInitSegmentUrl(trackId: number) {\n return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;\n }\n\n buildMediaSegmentUrl(trackId: number, _segmentId: number) {\n return `${this.apiHost}/api/v1/files/${this.fileId}/tracks/${trackId}`;\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for file-based media\",\n );\n }\n\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(`Track ${rendition.trackId} not found`);\n }\n\n const { offset, size } = trackData.initSegment;\n const url = this.buildInitSegmentUrl(rendition.trackId);\n const fullTrack = await this.fetchMedia(url, signal);\n return fullTrack.slice(offset, offset + size);\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for file-based media\",\n );\n }\n\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(`Track ${rendition.trackId} not found`);\n }\n\n const segment = trackData.segments[segmentId];\n if (!segment) {\n throw new Error(\n `Segment ${segmentId} not found for track ${rendition.trackId}`,\n );\n }\n\n const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);\n const fullTrack = await this.fetchMedia(url, signal);\n return fullTrack.slice(segment.offset, segment.offset + segment.size);\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n if (!rendition.trackId) {\n throw new Error(\n \"[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata\",\n );\n }\n const trackData = this.data[rendition.trackId];\n if (!trackData) {\n throw new Error(\"Track not found\");\n }\n const segment = trackData.segments?.[segmentId];\n if (!segment) {\n throw new Error(\"Segment not found\");\n }\n const segmentStartMs = (segment.cts / trackData.timescale) * 1000;\n\n return globalTimestamps.map(\n (globalMs) => (globalMs - segmentStartMs) / 1000,\n );\n }\n}\n\n/** @deprecated Use FileMediaEngine instead */\nexport const AssetIdMediaEngine = FileMediaEngine;\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseMediaEngine } from "./BaseMediaEngine.js";
|
|
2
1
|
import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
|
|
2
|
+
import { BaseMediaEngine, mediaCache } from "./BaseMediaEngine.js";
|
|
3
3
|
|
|
4
4
|
//#region src/elements/EFMedia/JitMediaEngine.ts
|
|
5
5
|
var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
@@ -102,6 +102,15 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
102
102
|
if (segmentIndex * rendition.segmentDurationMs >= this.durationMs) return;
|
|
103
103
|
return segmentIndex + 1;
|
|
104
104
|
}
|
|
105
|
+
getBufferConfig() {
|
|
106
|
+
return {
|
|
107
|
+
videoBufferDurationMs: 4e3,
|
|
108
|
+
audioBufferDurationMs: 4e3,
|
|
109
|
+
maxVideoBufferFetches: 2,
|
|
110
|
+
maxAudioBufferFetches: 2,
|
|
111
|
+
bufferThresholdMs: 3e4
|
|
112
|
+
};
|
|
113
|
+
}
|
|
105
114
|
getScrubVideoRendition() {
|
|
106
115
|
if (!this.data.videoRenditions) return void 0;
|
|
107
116
|
const scrubManifestRendition = this.data.videoRenditions.find((r) => r.id === "scrub");
|
|
@@ -114,18 +123,10 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
114
123
|
segmentDurationsMs: scrubManifestRendition.segmentDurationsMs
|
|
115
124
|
};
|
|
116
125
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
getBufferConfig() {
|
|
122
|
-
return {
|
|
123
|
-
videoBufferDurationMs: 8e3,
|
|
124
|
-
audioBufferDurationMs: 8e3,
|
|
125
|
-
maxVideoBufferFetches: 3,
|
|
126
|
-
maxAudioBufferFetches: 3,
|
|
127
|
-
bufferThresholdMs: 3e4
|
|
128
|
-
};
|
|
126
|
+
isSegmentCached(segmentId, rendition) {
|
|
127
|
+
if (!rendition.id) return false;
|
|
128
|
+
const segmentUrl = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
|
|
129
|
+
return mediaCache.has(segmentUrl);
|
|
129
130
|
}
|
|
130
131
|
/**
|
|
131
132
|
* Extract thumbnail canvases using same rendition priority as video playback for frame alignment
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JitMediaEngine.js","names":["#cachedAudioRendition","#cachedVideoRendition","rendition: VideoRendition"],"sources":["../../../src/elements/EFMedia/JitMediaEngine.ts"],"sourcesContent":["import type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport { BaseMediaEngine } from \"./BaseMediaEngine\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class JitMediaEngine extends BaseMediaEngine implements MediaEngine {\n private urlGenerator: UrlGenerator;\n private data: ManifestResponse = {} as ManifestResponse;\n private thumbnailExtractor: ThumbnailExtractor;\n\n static async fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string, signal?: AbortSignal) {\n const engine = new JitMediaEngine(host, urlGenerator);\n const data = await engine.fetchManifest(url, signal);\n \n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n \n engine.data = data;\n // Set MediaEngine interface properties\n engine.durationMs = data.durationMs;\n engine.src = data.sourceUrl;\n engine.templates = data.endpoints;\n return engine;\n }\n\n // MediaEngine interface properties\n durationMs = 0;\n src = \"\";\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, urlGenerator: UrlGenerator) {\n super(host);\n this.urlGenerator = urlGenerator;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\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 // Implement abstract methods required by BaseMediaEngine\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n get audioRendition(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.audioRenditions[0];\n if (!rendition) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedAudioRendition;\n }\n\n get videoRendition(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.videoRenditions[0];\n if (!rendition) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n \"init\",\n rendition.id,\n this,\n );\n\n // Use unified fetch method\n return this.fetchMedia(url, signal);\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n this,\n );\n return this.fetchMedia(url, signal);\n }\n\n computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: VideoRendition | AudioRendition,\n ) {\n // Don't request segments beyond the actual file duration\n // Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)\n if (desiredSeekTimeMs > this.durationMs) {\n return undefined;\n }\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 throw new Error(\"Segment duration is required for JIT metadata\");\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = cumulativeTime + segmentDuration;\n\n // Check if the desired seek time falls within this segment\n // Special case: for the last segment, include the exact end time\n const isLastSegment = i === rendition.segmentDurationsMs.length - 1;\n const includesEndTime =\n isLastSegment && desiredSeekTimeMs === this.durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1; // Convert 0-based to 1-based segment ID\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= this.durationMs) {\n break;\n }\n }\n\n // If we didn't find a segment, return undefined\n return undefined;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n if (!rendition.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(\n desiredSeekTimeMs / rendition.segmentDurationMs,\n );\n\n // Calculate the actual segment start time\n const segmentStartMs = segmentIndex * rendition.segmentDurationMs;\n\n // If this segment would start at or beyond file duration, it doesn't exist\n if (segmentStartMs >= this.durationMs) {\n return undefined;\n }\n\n return segmentIndex + 1; // Convert 0-based to 1-based\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n if (!this.data.videoRenditions) return undefined;\n\n const scrubManifestRendition = this.data.videoRenditions.find(\n (r) => r.id === \"scrub\",\n );\n\n if (!scrubManifestRendition) return this.getVideoRenditionInternal(); // Fallback to main\n\n return {\n id: scrubManifestRendition.id as any,\n trackId: undefined,\n src: this.src,\n segmentDurationMs: scrubManifestRendition.segmentDurationMs,\n segmentDurationsMs: scrubManifestRendition.segmentDurationsMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for JIT transcoding\n * Uses higher buffering since transcoding introduces latency\n */\n getBufferConfig() {\n return {\n // Buffer more aggressively for JIT transcoding to smooth over latency\n videoBufferDurationMs: 8000,\n audioBufferDurationMs: 8000,\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using same rendition priority as video playback for frame alignment\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use same rendition priority as video: try main rendition first for frame alignment\n let rendition: VideoRendition;\n try {\n const mainRendition = this.getVideoRenditionInternal();\n if (mainRendition) {\n rendition = mainRendition;\n } else {\n const scrubRendition = this.getScrubVideoRendition();\n if (scrubRendition) {\n rendition = scrubRendition;\n } else {\n throw new Error(\"No video rendition available\");\n }\n }\n } catch (error) {\n console.warn(\n \"JitMediaEngine: No video rendition available for thumbnails\",\n error,\n );\n return timestamps.map(() => null);\n }\n\n // Use shared thumbnail extraction logic\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 return globalTimestamps.map((timestamp) => timestamp / 1000);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JitMediaEngine.js","names":["#cachedAudioRendition","#cachedVideoRendition","rendition: VideoRendition"],"sources":["../../../src/elements/EFMedia/JitMediaEngine.ts"],"sourcesContent":["import type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { ManifestResponse } from \"../../transcoding/types/index.js\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class JitMediaEngine extends BaseMediaEngine implements MediaEngine {\n private urlGenerator: UrlGenerator;\n private data: ManifestResponse = {} as ManifestResponse;\n private thumbnailExtractor: ThumbnailExtractor;\n\n static async fetch(\n host: EFMedia,\n urlGenerator: UrlGenerator,\n url: string,\n signal?: AbortSignal,\n ) {\n const engine = new JitMediaEngine(host, urlGenerator);\n const data = await engine.fetchManifest(url, signal);\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n engine.data = data;\n // Set MediaEngine interface properties\n engine.durationMs = data.durationMs;\n engine.src = data.sourceUrl;\n engine.templates = data.endpoints;\n return engine;\n }\n\n // MediaEngine interface properties\n durationMs = 0;\n src = \"\";\n templates!: { initSegment: string; mediaSegment: string };\n\n constructor(host: EFMedia, urlGenerator: UrlGenerator) {\n super(host);\n this.urlGenerator = urlGenerator;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\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 // Implement abstract methods required by BaseMediaEngine\n protected getVideoRenditionInternal(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n protected getAudioRenditionInternal(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n get audioRendition(): AudioRendition | undefined {\n if (this.#cachedAudioRendition !== null) {\n return this.#cachedAudioRendition;\n }\n if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.audioRenditions[0];\n if (!rendition) {\n this.#cachedAudioRendition = undefined;\n return undefined;\n }\n\n this.#cachedAudioRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedAudioRendition;\n }\n\n get videoRendition(): VideoRendition | undefined {\n if (this.#cachedVideoRendition !== null) {\n return this.#cachedVideoRendition;\n }\n if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n const rendition = this.data.videoRenditions[0];\n if (!rendition) {\n this.#cachedVideoRendition = undefined;\n return undefined;\n }\n\n this.#cachedVideoRendition = {\n id: rendition.id as RenditionId,\n trackId: undefined,\n src: this.data.sourceUrl,\n segmentDurationMs: rendition.segmentDurationMs,\n segmentDurationsMs: rendition.segmentDurationsMs,\n startTimeOffsetMs: rendition.startTimeOffsetMs,\n };\n return this.#cachedVideoRendition;\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n \"init\",\n rendition.id,\n this,\n );\n\n // Use unified fetch method\n return this.fetchMedia(url, signal);\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n if (!rendition.id) {\n throw new Error(\"Rendition ID is required for JIT metadata\");\n }\n const url = this.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n this,\n );\n return this.fetchMedia(url, signal);\n }\n\n computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: VideoRendition | AudioRendition,\n ) {\n // Don't request segments beyond the actual file duration\n // Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)\n if (desiredSeekTimeMs > this.durationMs) {\n return undefined;\n }\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 throw new Error(\"Segment duration is required for JIT metadata\");\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = cumulativeTime + segmentDuration;\n\n // Check if the desired seek time falls within this segment\n // Special case: for the last segment, include the exact end time\n const isLastSegment = i === rendition.segmentDurationsMs.length - 1;\n const includesEndTime =\n isLastSegment && desiredSeekTimeMs === this.durationMs;\n\n if (\n desiredSeekTimeMs >= segmentStartMs &&\n (desiredSeekTimeMs < segmentEndMs || includesEndTime)\n ) {\n return i + 1; // Convert 0-based to 1-based segment ID\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= this.durationMs) {\n break;\n }\n }\n\n // If we didn't find a segment, return undefined\n return undefined;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n if (!rendition.segmentDurationMs) {\n throw new Error(\"Segment duration is required for JIT metadata\");\n }\n\n const segmentIndex = Math.floor(\n desiredSeekTimeMs / rendition.segmentDurationMs,\n );\n\n // Calculate the actual segment start time\n const segmentStartMs = segmentIndex * rendition.segmentDurationMs;\n\n // If this segment would start at or beyond file duration, it doesn't exist\n if (segmentStartMs >= this.durationMs) {\n return undefined;\n }\n\n return segmentIndex + 1; // Convert 0-based to 1-based\n }\n\n getBufferConfig() {\n return {\n videoBufferDurationMs: 4000,\n audioBufferDurationMs: 4000,\n maxVideoBufferFetches: 2,\n maxAudioBufferFetches: 2,\n bufferThresholdMs: 30000,\n };\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n if (!this.data.videoRenditions) return undefined;\n\n const scrubManifestRendition = this.data.videoRenditions.find(\n (r) => r.id === \"scrub\",\n );\n\n if (!scrubManifestRendition) return this.getVideoRenditionInternal(); // Fallback to main\n\n return {\n id: scrubManifestRendition.id as any,\n trackId: undefined,\n src: this.src,\n segmentDurationMs: scrubManifestRendition.segmentDurationMs,\n segmentDurationsMs: scrubManifestRendition.segmentDurationsMs,\n };\n }\n\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = this.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n this,\n );\n return mediaCache.has(segmentUrl);\n }\n\n /**\n * Extract thumbnail canvases using same rendition priority as video playback for frame alignment\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use same rendition priority as video: try main rendition first for frame alignment\n let rendition: VideoRendition;\n try {\n const mainRendition = this.getVideoRenditionInternal();\n if (mainRendition) {\n rendition = mainRendition;\n } else {\n const scrubRendition = this.getScrubVideoRendition();\n if (scrubRendition) {\n rendition = scrubRendition;\n } else {\n throw new Error(\"No video rendition available\");\n }\n }\n } catch (error) {\n console.warn(\n \"JitMediaEngine: No video rendition available for thumbnails\",\n error,\n );\n return timestamps.map(() => null);\n }\n\n // Use shared thumbnail extraction logic\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 return globalTimestamps.map((timestamp) => timestamp / 1000);\n }\n}\n"],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ const fetchAudioSegmentData = async (segmentIds, mediaEngine, signal) => {
|
|
|
11
11
|
return [segmentId, await mediaEngine.fetchMediaSegment(segmentId, audioRendition, signal)];
|
|
12
12
|
});
|
|
13
13
|
const fetchedSegments = await Promise.all(fetchPromises);
|
|
14
|
-
signal
|
|
14
|
+
signal.throwIfAborted();
|
|
15
15
|
for (const [segmentId, arrayBuffer] of fetchedSegments) segmentData.set(segmentId, arrayBuffer);
|
|
16
16
|
return segmentData;
|
|
17
17
|
};
|
|
@@ -30,10 +30,10 @@ const createAudioSpanBlob = (initSegment, mediaSegments) => {
|
|
|
30
30
|
const fetchAudioSpanningTime = async (host, fromMs, toMs, signal) => {
|
|
31
31
|
if (fromMs >= toMs || fromMs < 0) throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);
|
|
32
32
|
const mediaEngine = await host.getMediaEngine(signal);
|
|
33
|
-
signal
|
|
33
|
+
signal.throwIfAborted();
|
|
34
34
|
if (!mediaEngine?.audioRendition) return;
|
|
35
35
|
const initSegment = await mediaEngine.fetchInitSegment(mediaEngine.audioRendition, signal);
|
|
36
|
-
signal
|
|
36
|
+
signal.throwIfAborted();
|
|
37
37
|
if (!initSegment) return;
|
|
38
38
|
const segmentRanges = mediaEngine.calculateAudioSegmentRange(fromMs, toMs, mediaEngine.audioRendition, host.intrinsicDurationMs || 1e4);
|
|
39
39
|
if (segmentRanges.length === 0) throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type {\n AudioSpan,\n MediaEngine,\n SegmentTimeRange,\n} from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\n\n/**\n * Fetch audio segment data using MediaEngine\n * Pure function with explicit dependencies\n */\nconst fetchAudioSegmentData = async (\n segmentIds: number[],\n mediaEngine: MediaEngine,\n signal
|
|
1
|
+
{"version":3,"file":"AudioSpanUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/AudioSpanUtils.ts"],"sourcesContent":["import type {\n AudioSpan,\n MediaEngine,\n SegmentTimeRange,\n} from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\n\n/**\n * Fetch audio segment data using MediaEngine\n * Pure function with explicit dependencies\n */\nconst fetchAudioSegmentData = async (\n segmentIds: number[],\n mediaEngine: MediaEngine,\n signal: AbortSignal,\n): Promise<Map<number, ArrayBuffer>> => {\n const audioRendition = mediaEngine.audioRendition;\n if (!audioRendition) {\n throw new Error(\"Audio rendition not available\");\n }\n\n const segmentData = new Map<number, ArrayBuffer>();\n\n // Fetch all segments - MediaEngine handles deduplication internally\n const fetchPromises = segmentIds.map(async (segmentId) => {\n const arrayBuffer = await mediaEngine.fetchMediaSegment(\n segmentId,\n audioRendition,\n signal,\n );\n return [segmentId, arrayBuffer] as [number, ArrayBuffer];\n });\n\n const fetchedSegments = await Promise.all(fetchPromises);\n signal.throwIfAborted();\n\n for (const [segmentId, arrayBuffer] of fetchedSegments) {\n segmentData.set(segmentId, arrayBuffer);\n }\n\n return segmentData;\n};\n\n/**\n * Create audio span blob from init segment and media segments\n * Pure function for blob creation\n */\nconst createAudioSpanBlob = (\n initSegment: ArrayBuffer,\n mediaSegments: ArrayBuffer[],\n): Blob => {\n const chunks = [initSegment, ...mediaSegments];\n return new Blob(chunks, { type: \"audio/mp4\" });\n};\n\n/**\n * Fetch audio spanning a time range\n * Main function that orchestrates segment calculation, fetching, and blob creation\n */\nexport const fetchAudioSpanningTime = async (\n host: EFMedia,\n fromMs: number,\n toMs: number,\n signal: AbortSignal,\n): Promise<AudioSpan | undefined> => {\n // Validate inputs\n if (fromMs >= toMs || fromMs < 0) {\n throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);\n }\n\n // Get media engine using the new async method\n const mediaEngine = await host.getMediaEngine(signal);\n signal.throwIfAborted();\n\n // Return undefined if no audio rendition available\n if (!mediaEngine?.audioRendition) {\n return undefined;\n }\n\n // Fetch the init segment directly from media engine\n const initSegment = await mediaEngine.fetchInitSegment(\n mediaEngine.audioRendition,\n signal,\n );\n signal.throwIfAborted();\n\n if (!initSegment) {\n return undefined;\n }\n\n // Calculate segments needed using the media engine's method\n const segmentRanges = mediaEngine.calculateAudioSegmentRange(\n fromMs,\n toMs,\n mediaEngine.audioRendition,\n host.intrinsicDurationMs || 10000,\n );\n\n if (segmentRanges.length === 0) {\n throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);\n }\n\n // Fetch segment data\n const segmentIds = segmentRanges.map((r: SegmentTimeRange) => r.segmentId);\n const segmentData = await fetchAudioSegmentData(\n segmentIds,\n mediaEngine,\n signal,\n );\n\n // Create ordered array of segments\n const orderedSegments = segmentIds.map((id: number) => {\n const segment = segmentData.get(id);\n if (!segment) {\n throw new Error(`Missing segment data for segment ID ${id}`);\n }\n return segment;\n });\n\n // Create blob\n const blob = createAudioSpanBlob(initSegment, orderedSegments);\n\n // Calculate actual time boundaries\n const actualStartMs = Math.min(\n ...segmentRanges.map((r: SegmentTimeRange) => r.startMs),\n );\n const actualEndMs = Math.max(\n ...segmentRanges.map((r: SegmentTimeRange) => r.endMs),\n );\n\n return {\n startMs: actualStartMs,\n endMs: actualEndMs,\n blob,\n };\n};\n"],"mappings":";;;;;AAWA,MAAM,wBAAwB,OAC5B,YACA,aACA,WACsC;CACtC,MAAM,iBAAiB,YAAY;AACnC,KAAI,CAAC,eACH,OAAM,IAAI,MAAM,gCAAgC;CAGlD,MAAM,8BAAc,IAAI,KAA0B;CAGlD,MAAM,gBAAgB,WAAW,IAAI,OAAO,cAAc;AAMxD,SAAO,CAAC,WALY,MAAM,YAAY,kBACpC,WACA,gBACA,OACD,CAC8B;GAC/B;CAEF,MAAM,kBAAkB,MAAM,QAAQ,IAAI,cAAc;AACxD,QAAO,gBAAgB;AAEvB,MAAK,MAAM,CAAC,WAAW,gBAAgB,gBACrC,aAAY,IAAI,WAAW,YAAY;AAGzC,QAAO;;;;;;AAOT,MAAM,uBACJ,aACA,kBACS;CACT,MAAM,SAAS,CAAC,aAAa,GAAG,cAAc;AAC9C,QAAO,IAAI,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;;;;;;AAOhD,MAAa,yBAAyB,OACpC,MACA,QACA,MACA,WACmC;AAEnC,KAAI,UAAU,QAAQ,SAAS,EAC7B,OAAM,IAAI,MAAM,8BAA8B,OAAO,SAAS,OAAO;CAIvE,MAAM,cAAc,MAAM,KAAK,eAAe,OAAO;AACrD,QAAO,gBAAgB;AAGvB,KAAI,CAAC,aAAa,eAChB;CAIF,MAAM,cAAc,MAAM,YAAY,iBACpC,YAAY,gBACZ,OACD;AACD,QAAO,gBAAgB;AAEvB,KAAI,CAAC,YACH;CAIF,MAAM,gBAAgB,YAAY,2BAChC,QACA,MACA,YAAY,gBACZ,KAAK,uBAAuB,IAC7B;AAED,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,oCAAoC,OAAO,GAAG,KAAK,IAAI;CAIzE,MAAM,aAAa,cAAc,KAAK,MAAwB,EAAE,UAAU;CAC1E,MAAM,cAAc,MAAM,sBACxB,YACA,aACA,OACD;CAYD,MAAM,OAAO,oBAAoB,aATT,WAAW,KAAK,OAAe;EACrD,MAAM,UAAU,YAAY,IAAI,GAAG;AACnC,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,uCAAuC,KAAK;AAE9D,SAAO;GACP,CAG4D;AAU9D,QAAO;EACL,SARoB,KAAK,IACzB,GAAG,cAAc,KAAK,MAAwB,EAAE,QAAQ,CACzD;EAOC,OANkB,KAAK,IACvB,GAAG,cAAc,KAAK,MAAwB,EAAE,MAAM,CACvD;EAKC;EACD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { globalInputCache } from "./GlobalInputCache.js";
|
|
2
|
+
import { DEFAULT_MEDIABUNNY_TIMEOUT_MS, withTimeout } from "./timeoutUtils.js";
|
|
2
3
|
import { ALL_FORMATS, BlobSource, CanvasSink, Input } from "mediabunny";
|
|
3
4
|
|
|
4
5
|
//#region src/elements/EFMedia/shared/ThumbnailExtractor.ts
|
|
@@ -63,11 +64,11 @@ var ThumbnailExtractor = class {
|
|
|
63
64
|
const results = /* @__PURE__ */ new Map();
|
|
64
65
|
try {
|
|
65
66
|
signal?.throwIfAborted();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
const [initSegment, mediaSegment] = await Promise.all([
|
|
67
|
+
const initP = this.mediaEngine.fetchInitSegment(rendition, signal);
|
|
68
|
+
const mediaP = this.mediaEngine.fetchMediaSegment(segmentId, rendition, signal);
|
|
69
|
+
initP.catch(() => {});
|
|
70
|
+
mediaP.catch(() => {});
|
|
71
|
+
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
71
72
|
signal?.throwIfAborted();
|
|
72
73
|
const segmentBlob = new Blob([initSegment, mediaSegment]);
|
|
73
74
|
let input = globalInputCache.get(rendition.src, segmentId, rendition.id);
|
|
@@ -78,7 +79,7 @@ var ThumbnailExtractor = class {
|
|
|
78
79
|
});
|
|
79
80
|
globalInputCache.set(rendition.src, segmentId, input, rendition.id);
|
|
80
81
|
}
|
|
81
|
-
const videoTrack = await input.getPrimaryVideoTrack();
|
|
82
|
+
const videoTrack = await withTimeout(input.getPrimaryVideoTrack(), 5e3, "ThumbnailExtractor.getPrimaryVideoTrack", signal);
|
|
82
83
|
if (!videoTrack) {
|
|
83
84
|
for (const timestamp of timestamps) results.set(timestamp, null);
|
|
84
85
|
return results;
|
|
@@ -87,7 +88,11 @@ var ThumbnailExtractor = class {
|
|
|
87
88
|
const sortedTimestamps = [...timestamps].sort((a, b) => a - b);
|
|
88
89
|
const relativeTimestamps = this.convertToSegmentRelativeTimestamps(sortedTimestamps, segmentId, rendition);
|
|
89
90
|
const timestampResults = [];
|
|
90
|
-
|
|
91
|
+
const canvasIterator = sink.canvasesAtTimestamps(relativeTimestamps);
|
|
92
|
+
for await (const result of canvasIterator) {
|
|
93
|
+
const canvasResult = await withTimeout(Promise.resolve(result), DEFAULT_MEDIABUNNY_TIMEOUT_MS, "ThumbnailExtractor canvasesAtTimestamps iteration", signal);
|
|
94
|
+
timestampResults.push(canvasResult);
|
|
95
|
+
}
|
|
91
96
|
for (let i = 0; i < sortedTimestamps.length; i++) {
|
|
92
97
|
const globalTimestamp = sortedTimestamps[i];
|
|
93
98
|
if (globalTimestamp === void 0) continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: BaseMediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type {\n ThumbnailResult,\n VideoRendition,\n} from \"../../../transcoding/types/index.js\";\nimport type { BaseMediaEngine } from \"../BaseMediaEngine.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\n\n/**\n * Shared thumbnail extraction logic for all MediaEngine implementations\n * Eliminates code duplication and provides consistent behavior\n */\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: BaseMediaEngine) {}\n\n /**\n * Extract thumbnails at multiple timestamps efficiently using segment batching\n */\n async extractThumbnails(\n timestamps: number[],\n rendition: VideoRendition,\n durationMs: number,\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n // Validate and filter timestamps within bounds\n const validTimestamps = timestamps.filter(\n (timeMs) => timeMs >= 0 && timeMs <= durationMs,\n );\n\n if (validTimestamps.length === 0) {\n console.warn(\n `ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`,\n );\n return timestamps.map(() => null);\n }\n\n // Group timestamps by segment for batch processing\n const segmentGroups = this.groupTimestampsBySegment(\n validTimestamps,\n rendition,\n );\n\n // Extract batched by segment using CanvasSink\n const results = new Map<number, ThumbnailResult | null>();\n\n for (const [segmentId, segmentTimestamps] of segmentGroups) {\n // Check abort before processing each segment\n signal?.throwIfAborted();\n \n try {\n const segmentResults = await this.extractSegmentThumbnails(\n segmentId,\n segmentTimestamps,\n rendition,\n signal,\n );\n\n for (const [timestamp, thumbnail] of segmentResults) {\n results.set(timestamp, thumbnail);\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Mark all timestamps in this segment as failed\n for (const timestamp of segmentTimestamps) {\n results.set(timestamp, null);\n }\n }\n }\n\n // Return in original order, null for any that failed or were out of bounds\n return timestamps.map((t) => {\n // If timestamp was out of bounds, return null\n if (t < 0 || t > durationMs) {\n return null;\n }\n return results.get(t) || null;\n });\n }\n\n /**\n * Group timestamps by segment ID for efficient batch processing\n */\n private groupTimestampsBySegment(\n timestamps: number[],\n rendition: VideoRendition,\n ): Map<number, number[]> {\n const segmentGroups = new Map<number, number[]>();\n\n for (const timeMs of timestamps) {\n try {\n const segmentId = this.mediaEngine.computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n if (!segmentGroups.has(segmentId)) {\n segmentGroups.set(segmentId, []);\n }\n const segmentGroup = segmentGroups.get(segmentId) ?? [];\n if (!segmentGroup) {\n segmentGroups.set(segmentId, []);\n }\n segmentGroup.push(timeMs);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`,\n error,\n );\n }\n }\n\n return segmentGroups;\n }\n\n /**\n * Extract thumbnails for a specific segment using CanvasSink\n */\n private async extractSegmentThumbnails(\n segmentId: number,\n timestamps: number[],\n rendition: VideoRendition,\n signal?: AbortSignal,\n ): Promise<Map<number, ThumbnailResult | null>> {\n const results = new Map<number, ThumbnailResult | null>();\n\n try {\n // Check abort before starting segment fetch\n signal?.throwIfAborted();\n \n // Get segment data through existing media engine methods (uses caches)\n // Note: fetchInitSegment requires a signal, so signal must be provided\n // If no signal provided, we cannot abort the operation, so skip it\n if (!signal) {\n // Return nulls for all timestamps when signal is not provided\n // This ensures we don't create orphan signals that can never be aborted\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n \n const [initSegment, mediaSegment] = await Promise.all([\n this.mediaEngine.fetchInitSegment(rendition, signal),\n this.mediaEngine.fetchMediaSegment(segmentId, rendition, signal),\n ]);\n \n // Check abort after potentially slow network operations\n signal?.throwIfAborted();\n\n // Create Input for this segment using global shared cache\n const segmentBlob = new Blob([initSegment, mediaSegment]);\n\n let input = globalInputCache.get(rendition.src, segmentId, rendition.id);\n if (!input) {\n input = new Input({\n formats: ALL_FORMATS,\n source: new BlobSource(segmentBlob),\n });\n globalInputCache.set(rendition.src, segmentId, input, rendition.id);\n }\n\n // Set up CanvasSink for batched extraction\n const videoTrack = await input.getPrimaryVideoTrack();\n if (!videoTrack) {\n // No video track - return nulls for all timestamps\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n\n const sink = new CanvasSink(videoTrack);\n\n // IMPORTANT: Sort timestamps for mediabunny - it expects monotonically sorted timestamps\n // Create array of {original, sorted} to map back after extraction\n const sortedTimestamps = [...timestamps].sort((a, b) => a - b);\n\n // Convert sorted global timestamps to segment-relative (in seconds for mediabunny)\n const relativeTimestamps = this.convertToSegmentRelativeTimestamps(\n sortedTimestamps,\n segmentId,\n rendition,\n );\n\n // Batch extract all thumbnails for this segment (in sorted order)\n const timestampResults = [];\n for await (const result of sink.canvasesAtTimestamps(\n relativeTimestamps,\n )) {\n timestampResults.push(result);\n }\n\n // Map results back to original (sorted) timestamps\n for (let i = 0; i < sortedTimestamps.length; i++) {\n const globalTimestamp = sortedTimestamps[i];\n if (globalTimestamp === undefined) {\n continue;\n }\n\n const result = timestampResults[i];\n\n if (result?.canvas) {\n const canvas = result.canvas;\n if (\n canvas instanceof HTMLCanvasElement ||\n canvas instanceof OffscreenCanvas\n ) {\n results.set(globalTimestamp, {\n timestamp: globalTimestamp,\n thumbnail: canvas,\n });\n } else {\n results.set(globalTimestamp, null);\n }\n } else {\n results.set(globalTimestamp, null);\n }\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Thumbnail extraction can fail for various non-fatal reasons (network issues, \n // missing segments, transcoding not ready). Log as warning and return nulls.\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Return nulls for all timestamps on error\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n }\n\n return results;\n }\n\n /**\n * Convert global timestamps to segment-relative timestamps for mediabunny\n * This is where the main difference between JIT and Asset engines lies\n */\n private convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n return this.mediaEngine.convertToSegmentRelativeTimestamps(\n globalTimestamps,\n segmentId,\n rendition,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ThumbnailExtractor.js","names":["mediaEngine: BaseMediaEngine"],"sources":["../../../../src/elements/EFMedia/shared/ThumbnailExtractor.ts"],"sourcesContent":["import { ALL_FORMATS, BlobSource, CanvasSink, Input } from \"mediabunny\";\nimport type {\n ThumbnailResult,\n VideoRendition,\n} from \"../../../transcoding/types/index.js\";\nimport type { BaseMediaEngine } from \"../BaseMediaEngine.js\";\nimport { globalInputCache } from \"./GlobalInputCache.js\";\nimport { withTimeout, DEFAULT_MEDIABUNNY_TIMEOUT_MS } from \"./timeoutUtils.js\";\n\n/**\n * Shared thumbnail extraction logic for all MediaEngine implementations\n * Eliminates code duplication and provides consistent behavior\n */\nexport class ThumbnailExtractor {\n constructor(private mediaEngine: BaseMediaEngine) {}\n\n /**\n * Extract thumbnails at multiple timestamps efficiently using segment batching\n */\n async extractThumbnails(\n timestamps: number[],\n rendition: VideoRendition,\n durationMs: number,\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n if (timestamps.length === 0) {\n return [];\n }\n\n // Validate and filter timestamps within bounds\n const validTimestamps = timestamps.filter(\n (timeMs) => timeMs >= 0 && timeMs <= durationMs,\n );\n\n if (validTimestamps.length === 0) {\n console.warn(\n `ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`,\n );\n return timestamps.map(() => null);\n }\n\n // Group timestamps by segment for batch processing\n const segmentGroups = this.groupTimestampsBySegment(\n validTimestamps,\n rendition,\n );\n\n // Extract batched by segment using CanvasSink\n const results = new Map<number, ThumbnailResult | null>();\n\n for (const [segmentId, segmentTimestamps] of segmentGroups) {\n // Check abort before processing each segment\n signal?.throwIfAborted();\n\n try {\n const segmentResults = await this.extractSegmentThumbnails(\n segmentId,\n segmentTimestamps,\n rendition,\n signal,\n );\n\n for (const [timestamp, thumbnail] of segmentResults) {\n results.set(timestamp, thumbnail);\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Mark all timestamps in this segment as failed\n for (const timestamp of segmentTimestamps) {\n results.set(timestamp, null);\n }\n }\n }\n\n // Return in original order, null for any that failed or were out of bounds\n return timestamps.map((t) => {\n // If timestamp was out of bounds, return null\n if (t < 0 || t > durationMs) {\n return null;\n }\n return results.get(t) || null;\n });\n }\n\n /**\n * Group timestamps by segment ID for efficient batch processing\n */\n private groupTimestampsBySegment(\n timestamps: number[],\n rendition: VideoRendition,\n ): Map<number, number[]> {\n const segmentGroups = new Map<number, number[]>();\n\n for (const timeMs of timestamps) {\n try {\n const segmentId = this.mediaEngine.computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n if (!segmentGroups.has(segmentId)) {\n segmentGroups.set(segmentId, []);\n }\n const segmentGroup = segmentGroups.get(segmentId) ?? [];\n if (!segmentGroup) {\n segmentGroups.set(segmentId, []);\n }\n segmentGroup.push(timeMs);\n }\n } catch (error) {\n console.warn(\n `ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`,\n error,\n );\n }\n }\n\n return segmentGroups;\n }\n\n /**\n * Extract thumbnails for a specific segment using CanvasSink\n */\n private async extractSegmentThumbnails(\n segmentId: number,\n timestamps: number[],\n rendition: VideoRendition,\n signal?: AbortSignal,\n ): Promise<Map<number, ThumbnailResult | null>> {\n const results = new Map<number, ThumbnailResult | null>();\n\n try {\n // Check abort before starting segment fetch\n signal?.throwIfAborted();\n\n const initP = this.mediaEngine.fetchInitSegment(rendition, signal!);\n const mediaP = this.mediaEngine.fetchMediaSegment(\n segmentId,\n rendition,\n signal!,\n );\n initP.catch(() => {});\n mediaP.catch(() => {});\n const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);\n\n // Check abort after potentially slow network operations\n signal?.throwIfAborted();\n\n // Create Input for this segment using global shared cache\n const segmentBlob = new Blob([initSegment, mediaSegment]);\n\n let input = globalInputCache.get(rendition.src, segmentId, rendition.id);\n if (!input) {\n input = new Input({\n formats: ALL_FORMATS,\n source: new BlobSource(segmentBlob),\n });\n globalInputCache.set(rendition.src, segmentId, input, rendition.id);\n }\n\n // Set up CanvasSink for batched extraction\n const videoTrack = await withTimeout(\n input.getPrimaryVideoTrack(),\n 5000,\n \"ThumbnailExtractor.getPrimaryVideoTrack\",\n signal,\n );\n if (!videoTrack) {\n // No video track - return nulls for all timestamps\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n return results;\n }\n\n const sink = new CanvasSink(videoTrack);\n\n // IMPORTANT: Sort timestamps for mediabunny - it expects monotonically sorted timestamps\n // Create array of {original, sorted} to map back after extraction\n const sortedTimestamps = [...timestamps].sort((a, b) => a - b);\n\n // Convert sorted global timestamps to segment-relative (in seconds for mediabunny)\n const relativeTimestamps = this.convertToSegmentRelativeTimestamps(\n sortedTimestamps,\n segmentId,\n rendition,\n );\n\n // Batch extract all thumbnails for this segment (in sorted order)\n const timestampResults = [];\n const canvasIterator = sink.canvasesAtTimestamps(relativeTimestamps);\n for await (const result of canvasIterator) {\n // Wrap each iteration with timeout to prevent hangs\n const canvasResult = await withTimeout(\n Promise.resolve(result),\n DEFAULT_MEDIABUNNY_TIMEOUT_MS,\n \"ThumbnailExtractor canvasesAtTimestamps iteration\",\n signal,\n );\n timestampResults.push(canvasResult);\n }\n\n // Map results back to original (sorted) timestamps\n for (let i = 0; i < sortedTimestamps.length; i++) {\n const globalTimestamp = sortedTimestamps[i];\n if (globalTimestamp === undefined) {\n continue;\n }\n\n const result = timestampResults[i];\n\n if (result?.canvas) {\n const canvas = result.canvas;\n if (\n canvas instanceof HTMLCanvasElement ||\n canvas instanceof OffscreenCanvas\n ) {\n results.set(globalTimestamp, {\n timestamp: globalTimestamp,\n thumbnail: canvas,\n });\n } else {\n results.set(globalTimestamp, null);\n }\n } else {\n results.set(globalTimestamp, null);\n }\n }\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Thumbnail extraction can fail for various non-fatal reasons (network issues,\n // missing segments, transcoding not ready). Log as warning and return nulls.\n console.warn(\n `ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`,\n error,\n );\n // Return nulls for all timestamps on error\n for (const timestamp of timestamps) {\n results.set(timestamp, null);\n }\n }\n\n return results;\n }\n\n /**\n * Convert global timestamps to segment-relative timestamps for mediabunny\n * This is where the main difference between JIT and Asset engines lies\n */\n private convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n return this.mediaEngine.convertToSegmentRelativeTimestamps(\n globalTimestamps,\n segmentId,\n rendition,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import opentelemetry, { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/elements/EFMedia/shared/timeoutUtils.ts
|
|
4
|
+
const DEFAULT_MEDIABUNNY_TIMEOUT_MS = 1e4;
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a promise with a timeout and optional AbortSignal support.
|
|
7
|
+
* Records errors in OpenTelemetry spans for observability.
|
|
8
|
+
*
|
|
9
|
+
* @param promise - The promise to wrap with timeout protection
|
|
10
|
+
* @param timeoutMs - Timeout duration in milliseconds
|
|
11
|
+
* @param operationName - Name of the operation for error messages and telemetry
|
|
12
|
+
* @param signal - Optional AbortSignal for cancellation support
|
|
13
|
+
* @returns The result of the promise if it completes before timeout
|
|
14
|
+
* @throws Error if timeout is reached or operation is aborted
|
|
15
|
+
*/
|
|
16
|
+
async function withTimeout(promise, timeoutMs, operationName, signal) {
|
|
17
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
18
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
19
|
+
const timeoutId = setTimeout(() => {
|
|
20
|
+
reject(/* @__PURE__ */ new Error(`${operationName} timeout after ${timeoutMs}ms. This may indicate a mediabunny decoding issue or missing codec configuration. Check video codec configuration, try different quality, or contact support.`));
|
|
21
|
+
}, timeoutMs);
|
|
22
|
+
signal?.addEventListener("abort", () => {
|
|
23
|
+
clearTimeout(timeoutId);
|
|
24
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
25
|
+
}, { once: true });
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const span = opentelemetry.trace.getActiveSpan();
|
|
31
|
+
if (span && error instanceof Error) {
|
|
32
|
+
span.recordException(error);
|
|
33
|
+
span.setStatus({
|
|
34
|
+
code: SpanStatusCode.ERROR,
|
|
35
|
+
message: error.message
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { DEFAULT_MEDIABUNNY_TIMEOUT_MS, withTimeout };
|
|
44
|
+
//# sourceMappingURL=timeoutUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeoutUtils.js","names":[],"sources":["../../../../src/elements/EFMedia/shared/timeoutUtils.ts"],"sourcesContent":["import opentelemetry, { SpanStatusCode } from \"@opentelemetry/api\";\n\nexport const DEFAULT_MEDIABUNNY_TIMEOUT_MS = 10000; // 10s for decode operations\n\n/**\n * Wraps a promise with a timeout and optional AbortSignal support.\n * Records errors in OpenTelemetry spans for observability.\n *\n * @param promise - The promise to wrap with timeout protection\n * @param timeoutMs - Timeout duration in milliseconds\n * @param operationName - Name of the operation for error messages and telemetry\n * @param signal - Optional AbortSignal for cancellation support\n * @returns The result of the promise if it completes before timeout\n * @throws Error if timeout is reached or operation is aborted\n */\nexport async function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n operationName: string,\n signal?: AbortSignal,\n): Promise<T> {\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n const timeoutId = setTimeout(() => {\n const error = new Error(\n `${operationName} timeout after ${timeoutMs}ms. ` +\n `This may indicate a mediabunny decoding issue or missing codec configuration. ` +\n `Check video codec configuration, try different quality, or contact support.`,\n );\n reject(error);\n }, timeoutMs);\n\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n\n try {\n return await Promise.race([promise, timeoutPromise]);\n } catch (error) {\n // Record in active span if available\n const span = opentelemetry.trace.getActiveSpan();\n if (span && error instanceof Error) {\n span.recordException(error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n }\n throw error;\n }\n}\n"],"mappings":";;;AAEA,MAAa,gCAAgC;;;;;;;;;;;;AAa7C,eAAsB,YACpB,SACA,WACA,eACA,QACY;AACZ,KAAI,QAAQ,QACV,OAAM,IAAI,aAAa,WAAW,aAAa;CAGjD,MAAM,iBAAiB,IAAI,SAAgB,GAAG,WAAW;EACvD,MAAM,YAAY,iBAAiB;AAMjC,0BALc,IAAI,MAChB,GAAG,cAAc,iBAAiB,UAAU,+JAG7C,CACY;KACZ,UAAU;AAEb,UAAQ,iBACN,eACM;AACJ,gBAAa,UAAU;AACvB,UAAO,IAAI,aAAa,WAAW,aAAa,CAAC;KAEnD,EAAE,MAAM,MAAM,CACf;GACD;AAEF,KAAI;AACF,SAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,eAAe,CAAC;UAC7C,OAAO;EAEd,MAAM,OAAO,cAAc,MAAM,eAAe;AAChD,MAAI,QAAQ,iBAAiB,OAAO;AAClC,QAAK,gBAAgB,MAAM;AAC3B,QAAK,UAAU;IACb,MAAM,eAAe;IACrB,SAAS,MAAM;IAChB,CAAC;;AAEJ,QAAM"}
|
|
@@ -16,7 +16,7 @@ var MainVideoInputCache = class {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Get or create BufferedSeekingInput for a main video segment.
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* Uses promise deduplication to prevent race conditions when multiple
|
|
21
21
|
* concurrent requests arrive for the same segment. Without this,
|
|
22
22
|
* the first segment often fails when DevTools is closed because:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MainVideoInputCache.js","names":["#getCacheKey","#cache","#pendingPromises","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n #cache = new Map<string, BufferedSeekingInput>();\n #pendingPromises = new Map
|
|
1
|
+
{"version":3,"file":"MainVideoInputCache.js","names":["#getCacheKey","#cache","#pendingPromises","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/MainVideoInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for main video BufferedSeekingInput instances\n * Main video segments are typically 2s long, so we can reuse the same input\n * for multiple frames within that segment (e.g., 60 frames at 30fps)\n */\nexport class MainVideoInputCache {\n #cache = new Map<string, BufferedSeekingInput>();\n #pendingPromises = new Map<\n string,\n Promise<BufferedSeekingInput | undefined>\n >();\n #maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)\n\n /**\n * Create a cache key that uniquely identifies a segment\n */\n #getCacheKey(\n src: string,\n segmentId: number,\n renditionId: string | undefined,\n ): string {\n return `${src}:${renditionId || \"default\"}:${segmentId}`;\n }\n\n /**\n * Get or create BufferedSeekingInput for a main video segment.\n *\n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment. Without this,\n * the first segment often fails when DevTools is closed because:\n * 1. Video display and thumbnail extraction both request segment 0\n * 2. Both find cache empty and start createInputFn()\n * 3. Both create separate instances, causing conflicts\n */\n async getOrCreateInput(\n src: string,\n segmentId: number,\n renditionId: string | undefined,\n createInputFn: () => Promise<BufferedSeekingInput | undefined>,\n ): Promise<BufferedSeekingInput | undefined> {\n const cacheKey = this.#getCacheKey(src, segmentId, renditionId);\n\n // Check if we already have a completed result cached\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Check if there's already a pending request for this segment (deduplication!)\n // This prevents the race condition where multiple concurrent requests\n // each create their own BufferedSeekingInput instance.\n const pending = this.#pendingPromises.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n // Create the promise and cache it IMMEDIATELY to prevent race conditions\n const promise = createInputFn()\n .then((input) => {\n // Clean up pending promise\n this.#pendingPromises.delete(cacheKey);\n\n if (input) {\n // Add to completed cache\n this.#cache.set(cacheKey, input);\n\n // Evict oldest entries if cache is too large (LRU-like behavior)\n if (this.#cache.size > this.#maxCacheSize) {\n const oldestKey = this.#cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.#cache.delete(oldestKey);\n }\n }\n }\n\n return input;\n })\n .catch((error) => {\n // Clean up pending promise on failure so retry is possible\n this.#pendingPromises.delete(cacheKey);\n throw error;\n });\n\n this.#pendingPromises.set(cacheKey, promise);\n return promise;\n }\n\n /**\n * Clear the entire cache (called when video changes)\n */\n clear() {\n this.#cache.clear();\n this.#pendingPromises.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.#cache.size,\n pendingSize: this.#pendingPromises.size,\n cacheKeys: Array.from(this.#cache.keys()),\n };\n }\n}\n"],"mappings":";;;;;;AAOA,IAAa,sBAAb,MAAiC;CAC/B,yBAAS,IAAI,KAAmC;CAChD,mCAAmB,IAAI,KAGpB;CACH,gBAAgB;;;;CAKhB,aACE,KACA,WACA,aACQ;AACR,SAAO,GAAG,IAAI,GAAG,eAAe,UAAU,GAAG;;;;;;;;;;;;CAa/C,MAAM,iBACJ,KACA,WACA,aACA,eAC2C;EAC3C,MAAM,WAAW,MAAKA,YAAa,KAAK,WAAW,YAAY;EAG/D,MAAM,SAAS,MAAKC,MAAO,IAAI,SAAS;AACxC,MAAI,OACF,QAAO;EAMT,MAAM,UAAU,MAAKC,gBAAiB,IAAI,SAAS;AACnD,MAAI,QACF,QAAO;EAIT,MAAM,UAAU,eAAe,CAC5B,MAAM,UAAU;AAEf,SAAKA,gBAAiB,OAAO,SAAS;AAEtC,OAAI,OAAO;AAET,UAAKD,MAAO,IAAI,UAAU,MAAM;AAGhC,QAAI,MAAKA,MAAO,OAAO,MAAKE,cAAe;KACzC,MAAM,YAAY,MAAKF,MAAO,MAAM,CAAC,MAAM,CAAC;AAC5C,SAAI,cAAc,OAChB,OAAKA,MAAO,OAAO,UAAU;;;AAKnC,UAAO;IACP,CACD,OAAO,UAAU;AAEhB,SAAKC,gBAAiB,OAAO,SAAS;AACtC,SAAM;IACN;AAEJ,QAAKA,gBAAiB,IAAI,UAAU,QAAQ;AAC5C,SAAO;;;;;CAMT,QAAQ;AACN,QAAKD,MAAO,OAAO;AACnB,QAAKC,gBAAiB,OAAO;;;;;CAM/B,WAAW;AACT,SAAO;GACL,MAAM,MAAKD,MAAO;GAClB,aAAa,MAAKC,gBAAiB;GACnC,WAAW,MAAM,KAAK,MAAKD,MAAO,MAAM,CAAC;GAC1C"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/elements/EFMedia/videoTasks/ScrubInputCache.ts
|
|
2
2
|
/**
|
|
3
3
|
* Cache for scrub BufferedSeekingInput instances.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* For JIT media (segmented scrub tracks), caches by src + segment ID.
|
|
6
6
|
* For Asset media (single-file scrub tracks), caches by URL so all segments
|
|
7
7
|
* share the same BufferedSeekingInput instance.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* Uses promise deduplication to prevent race conditions when multiple
|
|
10
10
|
* concurrent requests arrive for the same segment.
|
|
11
11
|
*/
|
|
@@ -23,10 +23,10 @@ var ScrubInputCache = class {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Get or create BufferedSeekingInput for a scrub segment.
|
|
26
|
-
*
|
|
26
|
+
*
|
|
27
27
|
* Uses promise deduplication to prevent race conditions when multiple
|
|
28
28
|
* concurrent requests arrive for the same segment.
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
30
|
* @param src - The source URL of the video (required to distinguish between videos)
|
|
31
31
|
* @param segmentId - The segment ID
|
|
32
32
|
* @param createInputFn - Factory function to create the input
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScrubInputCache.js","names":["cached","#urlCache","pending","#pendingByUrl","promise","#getCacheKey","#cache","#pendingBySegment","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/ScrubInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for scrub BufferedSeekingInput instances.\n
|
|
1
|
+
{"version":3,"file":"ScrubInputCache.js","names":["cached","#urlCache","pending","#pendingByUrl","promise","#getCacheKey","#cache","#pendingBySegment","#maxCacheSize"],"sources":["../../../../src/elements/EFMedia/videoTasks/ScrubInputCache.ts"],"sourcesContent":["import type { BufferedSeekingInput } from \"../BufferedSeekingInput\";\n\n/**\n * Cache for scrub BufferedSeekingInput instances.\n *\n * For JIT media (segmented scrub tracks), caches by src + segment ID.\n * For Asset media (single-file scrub tracks), caches by URL so all segments\n * share the same BufferedSeekingInput instance.\n *\n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment.\n */\nexport class ScrubInputCache {\n // Changed from Map<number> to Map<string> to include src in key\n #cache = new Map<string, BufferedSeekingInput>();\n #urlCache = new Map<string, BufferedSeekingInput>();\n #pendingBySegment = new Map<\n string,\n Promise<BufferedSeekingInput | undefined>\n >();\n #pendingByUrl = new Map<string, Promise<BufferedSeekingInput | undefined>>();\n #maxCacheSize = 5;\n\n /**\n * Create a cache key that uniquely identifies a segment for a specific video\n */\n #getCacheKey(src: string, segmentId: number): string {\n return `${src}:${segmentId}`;\n }\n\n /**\n * Get or create BufferedSeekingInput for a scrub segment.\n *\n * Uses promise deduplication to prevent race conditions when multiple\n * concurrent requests arrive for the same segment.\n *\n * @param src - The source URL of the video (required to distinguish between videos)\n * @param segmentId - The segment ID\n * @param createInputFn - Factory function to create the input\n * @param scrubUrl - Optional URL for single-file scrub tracks (all segments share same input)\n */\n async getOrCreateInput(\n src: string,\n segmentId: number,\n createInputFn: () => Promise<BufferedSeekingInput | undefined>,\n scrubUrl?: string,\n ): Promise<BufferedSeekingInput | undefined> {\n // For single-file scrub tracks (AssetMediaEngine), use URL-based caching\n // This ensures all segments share the same BufferedSeekingInput\n if (scrubUrl) {\n // Check completed cache\n const cached = this.#urlCache.get(scrubUrl);\n if (cached) {\n return cached;\n }\n\n // Check pending requests (deduplication)\n const pending = this.#pendingByUrl.get(scrubUrl);\n if (pending) {\n return pending;\n }\n\n // Create promise and cache immediately\n const promise = createInputFn()\n .then((input) => {\n this.#pendingByUrl.delete(scrubUrl);\n if (input) {\n this.#urlCache.set(scrubUrl, input);\n }\n return input;\n })\n .catch((error) => {\n this.#pendingByUrl.delete(scrubUrl);\n throw error;\n });\n\n this.#pendingByUrl.set(scrubUrl, promise);\n return promise;\n }\n\n // For segmented scrub tracks (JIT), use src + segment-based caching\n const cacheKey = this.#getCacheKey(src, segmentId);\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Check pending requests (deduplication)\n const pending = this.#pendingBySegment.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n // Create promise and cache immediately\n const promise = createInputFn()\n .then((input) => {\n this.#pendingBySegment.delete(cacheKey);\n\n if (input) {\n this.#cache.set(cacheKey, input);\n\n // Evict oldest entries if cache is too large\n if (this.#cache.size > this.#maxCacheSize) {\n const oldestKey = this.#cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.#cache.delete(oldestKey);\n }\n }\n }\n\n return input;\n })\n .catch((error) => {\n this.#pendingBySegment.delete(cacheKey);\n throw error;\n });\n\n this.#pendingBySegment.set(cacheKey, promise);\n return promise;\n }\n\n /**\n * Clear the entire cache (called when video changes)\n */\n clear() {\n this.#cache.clear();\n this.#urlCache.clear();\n this.#pendingBySegment.clear();\n this.#pendingByUrl.clear();\n }\n\n /**\n * Get cache statistics\n */\n getStats() {\n return {\n size: this.#cache.size,\n urlCacheSize: this.#urlCache.size,\n pendingCount: this.#pendingBySegment.size + this.#pendingByUrl.size,\n cacheKeys: Array.from(this.#cache.keys()),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAa,kBAAb,MAA6B;CAE3B,yBAAS,IAAI,KAAmC;CAChD,4BAAY,IAAI,KAAmC;CACnD,oCAAoB,IAAI,KAGrB;CACH,gCAAgB,IAAI,KAAwD;CAC5E,gBAAgB;;;;CAKhB,aAAa,KAAa,WAA2B;AACnD,SAAO,GAAG,IAAI,GAAG;;;;;;;;;;;;;CAcnB,MAAM,iBACJ,KACA,WACA,eACA,UAC2C;AAG3C,MAAI,UAAU;GAEZ,MAAMA,WAAS,MAAKC,SAAU,IAAI,SAAS;AAC3C,OAAID,SACF,QAAOA;GAIT,MAAME,YAAU,MAAKC,aAAc,IAAI,SAAS;AAChD,OAAID,UACF,QAAOA;GAIT,MAAME,YAAU,eAAe,CAC5B,MAAM,UAAU;AACf,UAAKD,aAAc,OAAO,SAAS;AACnC,QAAI,MACF,OAAKF,SAAU,IAAI,UAAU,MAAM;AAErC,WAAO;KACP,CACD,OAAO,UAAU;AAChB,UAAKE,aAAc,OAAO,SAAS;AACnC,UAAM;KACN;AAEJ,SAAKA,aAAc,IAAI,UAAUC,UAAQ;AACzC,UAAOA;;EAIT,MAAM,WAAW,MAAKC,YAAa,KAAK,UAAU;EAClD,MAAM,SAAS,MAAKC,MAAO,IAAI,SAAS;AACxC,MAAI,OACF,QAAO;EAIT,MAAM,UAAU,MAAKC,iBAAkB,IAAI,SAAS;AACpD,MAAI,QACF,QAAO;EAIT,MAAM,UAAU,eAAe,CAC5B,MAAM,UAAU;AACf,SAAKA,iBAAkB,OAAO,SAAS;AAEvC,OAAI,OAAO;AACT,UAAKD,MAAO,IAAI,UAAU,MAAM;AAGhC,QAAI,MAAKA,MAAO,OAAO,MAAKE,cAAe;KACzC,MAAM,YAAY,MAAKF,MAAO,MAAM,CAAC,MAAM,CAAC;AAC5C,SAAI,cAAc,OAChB,OAAKA,MAAO,OAAO,UAAU;;;AAKnC,UAAO;IACP,CACD,OAAO,UAAU;AAChB,SAAKC,iBAAkB,OAAO,SAAS;AACvC,SAAM;IACN;AAEJ,QAAKA,iBAAkB,IAAI,UAAU,QAAQ;AAC7C,SAAO;;;;;CAMT,QAAQ;AACN,QAAKD,MAAO,OAAO;AACnB,QAAKL,SAAU,OAAO;AACtB,QAAKM,iBAAkB,OAAO;AAC9B,QAAKJ,aAAc,OAAO;;;;;CAM5B,WAAW;AACT,SAAO;GACL,MAAM,MAAKG,MAAO;GAClB,cAAc,MAAKL,SAAU;GAC7B,cAAc,MAAKM,iBAAkB,OAAO,MAAKJ,aAAc;GAC/D,WAAW,MAAM,KAAK,MAAKG,MAAO,MAAM,CAAC;GAC1C"}
|