@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +12 -2
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +118 -56
- package/dist/elements/EFThumbnailStrip.js +522 -358
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +223 -27
- package/dist/elements/EFTimegroup.js +850 -147
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +152 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +11 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +182 -4
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +840 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +68 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -1,33 +1,69 @@
|
|
|
1
1
|
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
2
|
-
import { BaseMediaEngine } from "./BaseMediaEngine.js";
|
|
2
|
+
import { BaseMediaEngine, mediaCache } from "./BaseMediaEngine.js";
|
|
3
3
|
import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
|
|
4
|
+
import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
|
|
4
5
|
|
|
5
6
|
//#region src/elements/EFMedia/AssetMediaEngine.ts
|
|
6
7
|
var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
7
|
-
constructor(host, src) {
|
|
8
|
+
constructor(host, src, urlGenerator) {
|
|
8
9
|
super(host);
|
|
9
10
|
this.data = {};
|
|
10
11
|
this.durationMs = 0;
|
|
11
12
|
this.src = src;
|
|
13
|
+
this.thumbnailExtractor = new ThumbnailExtractor(this);
|
|
14
|
+
this.urlGenerator = urlGenerator;
|
|
12
15
|
}
|
|
13
|
-
static async fetch(host, urlGenerator, src) {
|
|
14
|
-
const engine = new AssetMediaEngine(host, src);
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
static async fetch(host, urlGenerator, src, requiredTracks = "both", signal) {
|
|
17
|
+
const engine = new AssetMediaEngine(host, src, urlGenerator);
|
|
18
|
+
let normalizedSrc = src.startsWith("/") ? src.slice(1) : src;
|
|
19
|
+
normalizedSrc = normalizedSrc.replace(/^\/+/, "");
|
|
20
|
+
const baseUrl = urlGenerator.getBaseUrl();
|
|
21
|
+
const url = baseUrl ? `${baseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}` : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;
|
|
22
|
+
engine.data = await engine.fetchManifest(url, signal);
|
|
23
|
+
signal?.throwIfAborted();
|
|
17
24
|
engine.durationMs = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0) * 1e3;
|
|
18
25
|
if (src.startsWith("/")) engine.src = src.slice(1);
|
|
26
|
+
if (signal) {
|
|
27
|
+
const videoTrack = engine.videoTrackIndex;
|
|
28
|
+
const audioTrack = engine.audioTrackIndex;
|
|
29
|
+
const needsVideo = requiredTracks === "video" || requiredTracks === "both";
|
|
30
|
+
const needsAudio = requiredTracks === "audio" || requiredTracks === "both";
|
|
31
|
+
if (needsVideo && videoTrack && videoTrack.track !== void 0) try {
|
|
32
|
+
await engine.fetchInitSegment({
|
|
33
|
+
trackId: videoTrack.track,
|
|
34
|
+
src: engine.src
|
|
35
|
+
}, signal);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
38
|
+
if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") && error.message.includes("401"))) throw new Error(`Video segments require authentication: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
if (needsAudio && audioTrack && audioTrack.track !== void 0) try {
|
|
42
|
+
await engine.fetchInitSegment({
|
|
43
|
+
trackId: audioTrack.track,
|
|
44
|
+
src: engine.src
|
|
45
|
+
}, signal);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
48
|
+
if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") && error.message.includes("401"))) throw new Error(`Audio segments require authentication: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
19
51
|
return engine;
|
|
20
52
|
}
|
|
21
53
|
get audioTrackIndex() {
|
|
22
54
|
return Object.values(this.data).find((track) => track.type === "audio");
|
|
23
55
|
}
|
|
24
56
|
get videoTrackIndex() {
|
|
25
|
-
return Object.values(this.data).find((track) => track.type === "video");
|
|
57
|
+
return Object.values(this.data).find((track) => track.type === "video" && track.track !== void 0 && track.track > 0);
|
|
58
|
+
}
|
|
59
|
+
get scrubTrackIndex() {
|
|
60
|
+
return this.data[-1];
|
|
26
61
|
}
|
|
27
62
|
get videoRendition() {
|
|
28
63
|
const videoTrack = this.videoTrackIndex;
|
|
29
64
|
if (!videoTrack || videoTrack.track === void 0) return;
|
|
30
65
|
return {
|
|
66
|
+
id: "high",
|
|
31
67
|
trackId: videoTrack.track,
|
|
32
68
|
src: this.src,
|
|
33
69
|
startTimeOffsetMs: videoTrack.startTimeOffsetMs
|
|
@@ -37,49 +73,66 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
37
73
|
const audioTrack = this.audioTrackIndex;
|
|
38
74
|
if (!audioTrack || audioTrack.track === void 0) return;
|
|
39
75
|
return {
|
|
76
|
+
id: "audio",
|
|
40
77
|
trackId: audioTrack.track,
|
|
41
78
|
src: this.src
|
|
42
79
|
};
|
|
43
80
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Get the source URL for JIT format (needs to be absolute URL)
|
|
83
|
+
*/
|
|
84
|
+
getSourceUrlForJit() {
|
|
85
|
+
if (this.src.startsWith("http://") || this.src.startsWith("https://")) return this.src;
|
|
86
|
+
let baseUrl = this.urlGenerator.getBaseUrl();
|
|
87
|
+
if (!baseUrl) baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
88
|
+
const normalizedSrc = this.src.startsWith("/") ? this.src : `/${this.src}`;
|
|
89
|
+
return `${baseUrl}${normalizedSrc}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the base URL for constructing JIT endpoints
|
|
93
|
+
*/
|
|
94
|
+
getBaseUrlForJit() {
|
|
95
|
+
let baseUrl = this.urlGenerator.getBaseUrl();
|
|
96
|
+
if (!baseUrl) baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
97
|
+
return baseUrl;
|
|
57
98
|
}
|
|
58
99
|
get templates() {
|
|
100
|
+
const sourceUrl = this.getSourceUrlForJit();
|
|
101
|
+
const baseUrl = this.getBaseUrlForJit();
|
|
59
102
|
return {
|
|
60
|
-
initSegment:
|
|
61
|
-
mediaSegment:
|
|
103
|
+
initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
|
|
104
|
+
mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`
|
|
62
105
|
};
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Map trackId to JIT rendition ID for URL generation
|
|
109
|
+
* - trackId 1 (video) -> "high" (default video rendition)
|
|
110
|
+
* - trackId 2 (audio) -> "audio"
|
|
111
|
+
* - trackId -1 (scrub) -> "scrub"
|
|
112
|
+
*/
|
|
113
|
+
getRenditionId(trackId) {
|
|
114
|
+
if (trackId === -1) return "scrub";
|
|
115
|
+
if (trackId === 2) return "audio";
|
|
116
|
+
return "high";
|
|
66
117
|
}
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)
|
|
120
|
+
*/
|
|
121
|
+
isSegmentCached(segmentId, rendition) {
|
|
122
|
+
if (!rendition.id) return false;
|
|
123
|
+
const jitSegmentId = segmentId + 1;
|
|
124
|
+
const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);
|
|
125
|
+
return mediaCache.has(segmentUrl);
|
|
69
126
|
}
|
|
70
127
|
async fetchInitSegment(rendition, signal) {
|
|
71
128
|
return withSpan("assetEngine.fetchInitSegment", {
|
|
72
129
|
trackId: rendition.trackId || -1,
|
|
73
130
|
src: rendition.src
|
|
74
|
-
}, void 0, async (
|
|
131
|
+
}, void 0, async () => {
|
|
75
132
|
if (!rendition.trackId) throw new Error("[fetchInitSegment] Track ID is required for asset metadata");
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
span.setAttribute("offset", initSegment.offset);
|
|
80
|
-
span.setAttribute("size", initSegment.size);
|
|
81
|
-
const headers = { Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}` };
|
|
82
|
-
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
133
|
+
const renditionId = rendition.id || this.getRenditionId(rendition.trackId);
|
|
134
|
+
const url = this.urlGenerator.generateSegmentUrl("init", renditionId, this);
|
|
135
|
+
return this.fetchMedia(url, signal);
|
|
83
136
|
});
|
|
84
137
|
}
|
|
85
138
|
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
@@ -87,16 +140,13 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
87
140
|
segmentId,
|
|
88
141
|
trackId: rendition.trackId || -1,
|
|
89
142
|
src: rendition.src
|
|
90
|
-
}, void 0, async (
|
|
143
|
+
}, void 0, async () => {
|
|
91
144
|
if (!rendition.trackId) throw new Error("[fetchMediaSegment] Track ID is required for asset metadata");
|
|
92
145
|
if (segmentId === void 0) throw new Error("Segment ID is not available");
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
span.setAttribute("size", mediaSegment.size);
|
|
98
|
-
const headers = { Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}` };
|
|
99
|
-
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
146
|
+
const renditionId = rendition.id || this.getRenditionId(rendition.trackId);
|
|
147
|
+
const jitSegmentId = segmentId + 1;
|
|
148
|
+
const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);
|
|
149
|
+
return this.fetchMedia(url, signal);
|
|
100
150
|
});
|
|
101
151
|
}
|
|
102
152
|
/**
|
|
@@ -163,7 +213,22 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
163
213
|
}
|
|
164
214
|
return nearestSegmentIndex;
|
|
165
215
|
}
|
|
166
|
-
getScrubVideoRendition() {
|
|
216
|
+
getScrubVideoRendition() {
|
|
217
|
+
const scrubTrack = this.scrubTrackIndex;
|
|
218
|
+
if (!scrubTrack || scrubTrack.track === void 0) return;
|
|
219
|
+
const scrubSegmentDurationMs = 3e4;
|
|
220
|
+
const segmentDurationsMs = scrubTrack.segments.length > 0 ? scrubTrack.segments.map((segment) => {
|
|
221
|
+
return segment.duration / scrubTrack.timescale * 1e3;
|
|
222
|
+
}) : void 0;
|
|
223
|
+
return {
|
|
224
|
+
id: "scrub",
|
|
225
|
+
trackId: scrubTrack.track,
|
|
226
|
+
src: this.src,
|
|
227
|
+
segmentDurationMs: scrubSegmentDurationMs,
|
|
228
|
+
segmentDurationsMs,
|
|
229
|
+
startTimeOffsetMs: scrubTrack.startTimeOffsetMs
|
|
230
|
+
};
|
|
231
|
+
}
|
|
167
232
|
/**
|
|
168
233
|
* Get preferred buffer configuration for this media engine
|
|
169
234
|
* AssetMediaEngine uses lower buffering since segments are already optimized
|
|
@@ -177,16 +242,24 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
177
242
|
bufferThresholdMs: 3e4
|
|
178
243
|
};
|
|
179
244
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Extract thumbnail canvases using main video rendition
|
|
247
|
+
* Note: We prefer main video over scrub track because scrub track in AssetMediaEngine
|
|
248
|
+
* may have incomplete segment data that doesn't cover the full video duration.
|
|
249
|
+
*/
|
|
250
|
+
async extractThumbnails(timestamps, signal) {
|
|
251
|
+
const rendition = this.videoRendition;
|
|
252
|
+
if (!rendition) {
|
|
253
|
+
console.warn("AssetMediaEngine: No video rendition available for thumbnails");
|
|
254
|
+
return timestamps.map(() => null);
|
|
189
255
|
}
|
|
256
|
+
return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs, signal);
|
|
257
|
+
}
|
|
258
|
+
convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, rendition) {
|
|
259
|
+
const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;
|
|
260
|
+
return globalTimestamps.map((globalMs) => {
|
|
261
|
+
return (globalMs + startTimeOffsetMs) / 1e3;
|
|
262
|
+
});
|
|
190
263
|
}
|
|
191
264
|
};
|
|
192
265
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetMediaEngine.js","names":["paths: InitSegmentPaths","segmentRanges: SegmentTimeRange[]","distance: number"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n InitSegmentPaths,\n MediaEngine,\n SegmentTimeRange,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n\n constructor(host: EFMedia, src: string) {\n super(host);\n this.src = src;\n }\n\n static async fetch(host: EFMedia, urlGenerator: UrlGenerator, src: string) {\n const engine = new AssetMediaEngine(host, src);\n const url = urlGenerator.generateTrackFragmentIndexUrl(src);\n const data = await engine.fetchManifest(url);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n return engine;\n }\n\n get audioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n get videoTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"video\");\n }\n\n get videoRendition() {\n const videoTrack = this.videoTrackIndex;\n\n if (!videoTrack || videoTrack.track === undefined) {\n return undefined;\n }\n\n return {\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n get audioRendition() {\n const audioTrack = this.audioTrackIndex;\n\n if (!audioTrack || audioTrack.track === undefined) {\n return undefined;\n }\n\n return {\n trackId: audioTrack.track,\n src: this.src,\n };\n }\n\n get initSegmentPaths() {\n const paths: InitSegmentPaths = {};\n\n if (this.audioTrackIndex !== undefined) {\n paths.audio = {\n path: `@ef-track/${this.audioTrackIndex.track}.m4s`,\n pos: this.audioTrackIndex.initSegment.offset,\n size: this.audioTrackIndex.initSegment.size,\n };\n }\n\n if (this.videoTrackIndex !== undefined) {\n paths.video = {\n path: `/@ef-track/${this.videoTrackIndex.track}.m4s`,\n pos: this.videoTrackIndex.initSegment.offset,\n size: this.videoTrackIndex.initSegment.size,\n };\n }\n\n return paths;\n }\n\n get templates() {\n return {\n initSegment: \"/@ef-track/{src}?trackId={trackId}\",\n mediaSegment: \"/@ef-track/{src}?trackId={trackId}\",\n };\n }\n\n buildInitSegmentUrl(trackId: number) {\n return `/@ef-track/${this.src}?trackId=${trackId}`;\n }\n\n buildMediaSegmentUrl(trackId: number, segmentId: number) {\n return `/@ef-track/${this.src}?trackId=${trackId}&segmentId=${segmentId}`;\n }\n\n async fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async (span) => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n const url = this.buildInitSegmentUrl(rendition.trackId);\n const initSegment = this.data[rendition.trackId]?.initSegment;\n if (!initSegment) {\n throw new Error(\"Init segment not found\");\n }\n\n span.setAttribute(\"offset\", initSegment.offset);\n span.setAttribute(\"size\", initSegment.size);\n\n // Use unified fetch method with Range headers\n const headers = {\n Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}`,\n };\n\n return this.fetchMediaWithHeaders(url, headers, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async (span) => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);\n const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];\n if (!mediaSegment) {\n throw new Error(\"Media segment not found\");\n }\n\n span.setAttribute(\"offset\", mediaSegment.offset);\n span.setAttribute(\"size\", mediaSegment.size);\n\n // Use unified fetch method with Range headers\n const headers = {\n Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}`,\n };\n\n return this.fetchMediaWithHeaders(url, headers, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n // AssetMediaEngine does not have a dedicated scrub track\n return undefined;\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n // AssetMediaEngine inherits the default extractThumbnails from BaseMediaEngine\n // which provides a clear warning that this engine type is not supported\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n {\n // Asset: MediaBunny expects segment-relative timestamps in seconds\n // This is because Asset segments are independent timeline fragments\n\n if (!rendition.trackId) {\n throw new Error(\"Track ID is required for asset metadata\");\n }\n // For AssetMediaEngine, we need to calculate the actual segment start time\n // using the precise segment boundaries from the track fragment index\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetMediaEngine.js","names":["segmentRanges: SegmentTimeRange[]","distance: number","segmentDurationsMs: number[] | undefined"],"sources":["../../../src/elements/EFMedia/AssetMediaEngine.ts"],"sourcesContent":["import type { TrackFragmentIndex } from \"@editframe/assets\";\n\nimport { withSpan } from \"../../otel/tracingHelpers.js\";\nimport type {\n AudioRendition,\n MediaEngine,\n RenditionId,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport type { UrlGenerator } from \"../../transcoding/utils/UrlGenerator\";\nimport type { EFMedia } from \"../EFMedia\";\nimport { BaseMediaEngine, mediaCache } from \"./BaseMediaEngine\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils\";\nimport {\n convertToScaledTime,\n roundToMilliseconds,\n} from \"./shared/PrecisionUtils\";\nimport { ThumbnailExtractor } from \"./shared/ThumbnailExtractor.js\";\n\nexport class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {\n public src: string;\n protected data: Record<number, TrackFragmentIndex> = {};\n durationMs = 0;\n private thumbnailExtractor: ThumbnailExtractor;\n protected urlGenerator: UrlGenerator;\n\n constructor(host: EFMedia, src: string, urlGenerator: UrlGenerator) {\n super(host);\n this.src = src;\n this.thumbnailExtractor = new ThumbnailExtractor(this);\n this.urlGenerator = urlGenerator;\n }\n\n static async fetch(\n host: EFMedia, \n urlGenerator: UrlGenerator, \n src: string,\n requiredTracks: \"audio\" | \"video\" | \"both\" = \"both\",\n signal?: AbortSignal,\n ) {\n const engine = new AssetMediaEngine(host, src, urlGenerator);\n \n // Normalize the path: remove leading slash and any double slashes\n let normalizedSrc = src.startsWith(\"/\")\n ? src.slice(1)\n : src;\n normalizedSrc = normalizedSrc.replace(/^\\/+/, \"\");\n \n // Use production API format: /api/v1/isobmff_files/local/index?src={src}\n // This route is handled by the vite plugin for local development\n const baseUrl = urlGenerator.getBaseUrl();\n const url = baseUrl \n ? `${baseUrl}/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`\n : `/api/v1/isobmff_files/local/index?src=${encodeURIComponent(normalizedSrc)}`;\n const data = await engine.fetchManifest(url, signal);\n engine.data = data as Record<number, TrackFragmentIndex>;\n\n // Check for abort after potentially slow network operation\n signal?.throwIfAborted();\n\n // Calculate duration from the data\n const longestFragment = Object.values(engine.data).reduce(\n (max, fragment) => Math.max(max, fragment.duration / fragment.timescale),\n 0,\n );\n engine.durationMs = longestFragment * 1000;\n\n if (src.startsWith(\"/\")) {\n engine.src = src.slice(1);\n }\n\n // Validate that segments are accessible by trying to fetch the first init segment\n // This prevents creating a media engine that will fail on all subsequent segment fetches\n // If segments require authentication that's not available, fail early\n // Only validate tracks that are actually required by the consumer (e.g., EFAudio only needs audio)\n // Skip validation if no signal provided (backwards compatibility) - validation is optional\n if (signal) {\n const videoTrack = engine.videoTrackIndex;\n const audioTrack = engine.audioTrackIndex;\n const needsVideo = requiredTracks === \"video\" || requiredTracks === \"both\";\n const needsAudio = requiredTracks === \"audio\" || requiredTracks === \"both\";\n \n // Validate video track if required and available\n if (needsVideo && videoTrack && videoTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: videoTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Video segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n \n // Check for abort between validations\n signal?.throwIfAborted();\n \n // Validate audio track if required and available\n if (needsAudio && audioTrack && audioTrack.track !== undefined) {\n try {\n await engine.fetchInitSegment(\n { trackId: audioTrack.track, src: engine.src },\n signal,\n );\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If fetch fails with 401, segments require authentication that's not available\n // Fail media engine creation early to avoid all subsequent fetch calls\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n (error.message.includes(\"Failed to fetch\") && error.message.includes(\"401\")))\n ) {\n throw new Error(`Audio segments require authentication: ${error.message}`);\n }\n // For other errors (404, network errors, etc.), allow media engine creation\n // These might be transient or expected in some test scenarios\n }\n }\n }\n\n return engine;\n }\n\n get audioTrackIndex() {\n return Object.values(this.data).find((track) => track.type === \"audio\");\n }\n\n get videoTrackIndex() {\n return Object.values(this.data).find(\n (track) => track.type === \"video\" && track.track !== undefined && track.track > 0,\n );\n }\n\n get scrubTrackIndex() {\n // Scrub track uses track ID -1\n return this.data[-1];\n }\n\n get videoRendition() {\n const videoTrack = this.videoTrackIndex;\n\n if (!videoTrack || videoTrack.track === undefined) {\n return undefined;\n }\n\n return {\n id: \"high\" as RenditionId, // Use JIT-style rendition ID\n trackId: videoTrack.track,\n src: this.src,\n startTimeOffsetMs: videoTrack.startTimeOffsetMs,\n };\n }\n\n get audioRendition() {\n const audioTrack = this.audioTrackIndex;\n\n if (!audioTrack || audioTrack.track === undefined) {\n return undefined;\n }\n\n return {\n id: \"audio\" as RenditionId, // Use JIT-style rendition ID\n trackId: audioTrack.track,\n src: this.src,\n };\n }\n\n\n /**\n * Get the source URL for JIT format (needs to be absolute URL)\n */\n private getSourceUrlForJit(): string {\n // If src is already an absolute URL, use it\n if (this.src.startsWith(\"http://\") || this.src.startsWith(\"https://\")) {\n return this.src;\n }\n \n // Otherwise, construct absolute URL from baseUrl or current origin\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n // If src starts with /, keep it as-is (absolute path)\n // Otherwise, prepend with /\n const normalizedSrc = this.src.startsWith(\"/\") ? this.src : `/${this.src}`;\n return `${baseUrl}${normalizedSrc}`;\n }\n \n /**\n * Get the base URL for constructing JIT endpoints\n */\n private getBaseUrlForJit(): string {\n let baseUrl = this.urlGenerator.getBaseUrl();\n // If baseUrl is empty (no apiHost set), use current origin\n if (!baseUrl) {\n baseUrl = typeof window !== \"undefined\" ? window.location.origin : \"\";\n }\n return baseUrl;\n }\n\n get templates() {\n const sourceUrl = this.getSourceUrlForJit();\n const baseUrl = this.getBaseUrlForJit();\n return {\n initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n };\n }\n\n /**\n * Map trackId to JIT rendition ID for URL generation\n * - trackId 1 (video) -> \"high\" (default video rendition)\n * - trackId 2 (audio) -> \"audio\"\n * - trackId -1 (scrub) -> \"scrub\"\n */\n private getRenditionId(trackId: number): RenditionId {\n if (trackId === -1) return \"scrub\";\n if (trackId === 2) return \"audio\";\n return \"high\"; // Default video rendition (trackId 1)\n }\n\n /**\n * Override isSegmentCached to use URL-based cache checking (like JitMediaEngine)\n */\n override isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n // Use URL-based cache checking (same as JitMediaEngine)\n if (!rendition.id) {\n return false;\n }\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n const jitSegmentId = segmentId + 1;\n const segmentUrl = this.urlGenerator.generateSegmentUrl(jitSegmentId, rendition.id, this);\n return mediaCache.has(segmentUrl);\n }\n\n async fetchInitSegment(\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchInitSegment\",\n {\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchInitSegment] Track ID is required for asset metadata\",\n );\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n const url = this.urlGenerator.generateSegmentUrl(\"init\", renditionId, this);\n \n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n async fetchMediaSegment(\n segmentId: number,\n rendition: { id?: RenditionId; trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ) {\n return withSpan(\n \"assetEngine.fetchMediaSegment\",\n {\n segmentId,\n trackId: rendition.trackId || -1,\n src: rendition.src,\n },\n undefined,\n async () => {\n if (!rendition.trackId) {\n throw new Error(\n \"[fetchMediaSegment] Track ID is required for asset metadata\",\n );\n }\n if (segmentId === undefined) {\n throw new Error(\"Segment ID is not available\");\n }\n \n // Use rendition ID if provided, otherwise map from trackId\n const renditionId = rendition.id || this.getRenditionId(rendition.trackId);\n \n // JIT uses 1-based segment IDs, but AssetMediaEngine uses 0-based internally\n // So we need to add 1 to segmentId for the URL\n const jitSegmentId = segmentId + 1;\n const url = this.urlGenerator.generateSegmentUrl(jitSegmentId, renditionId, this);\n\n // Segments are now served directly (not via byte ranges), so use simple fetch\n return this.fetchMedia(url, signal);\n },\n );\n }\n\n /**\n * Calculate audio segments for variable-duration segments using track fragment index\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n _durationMs: number,\n ): SegmentTimeRange[] {\n if (fromMs >= toMs || !rendition.trackId) {\n console.warn(\n `calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const track = this.data[rendition.trackId];\n if (!track) {\n console.warn(\n `calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n return [];\n }\n\n const { timescale, segments } = track;\n const segmentRanges: SegmentTimeRange[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Convert to milliseconds\n const segmentStartMs = (segmentStartTime / timescale) * 1000;\n const segmentEndMs = (segmentEndTime / timescale) * 1000;\n\n // Check if segment overlaps with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segmentRanges.push({\n segmentId: i, // AssetMediaEngine uses 0-based segment IDs\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n if (segmentRanges.length === 0) {\n console.warn(\n `calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(\n {\n rendition,\n track,\n },\n )}`,\n );\n }\n\n return segmentRanges;\n }\n\n computeSegmentId(seekTimeMs: number, rendition: MediaRendition) {\n if (!rendition.trackId) {\n console.warn(\n `computeSegmentId: trackId not found for rendition ${JSON.stringify(\n rendition,\n )}`,\n );\n throw new Error(\n \"[computeSegmentId] Track ID is required for asset metadata\",\n );\n }\n const track = this.data[rendition.trackId];\n if (!track) {\n throw new Error(\"Track not found\");\n }\n const { timescale, segments } = track;\n\n // Apply startTimeOffsetMs to map user timeline to media timeline for segment selection\n const startTimeOffsetMs =\n (\"startTimeOffsetMs\" in rendition && rendition.startTimeOffsetMs) || 0;\n\n const offsetSeekTimeMs = roundToMilliseconds(\n seekTimeMs + startTimeOffsetMs,\n );\n // Convert to timescale units using consistent precision\n const scaledSeekTime = convertToScaledTime(offsetSeekTimeMs, timescale);\n\n // Find the segment that contains the actual seek time\n for (let i = segments.length - 1; i >= 0; i--) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentEndTime = segment.cts + segment.duration;\n\n // Check if the seek time falls within this segment\n if (segment.cts <= scaledSeekTime && scaledSeekTime < segmentEndTime) {\n return i;\n }\n }\n\n // Handle gaps: if no exact segment contains the time, find the nearest one\n // This handles cases where seek time falls between segments (like 8041.667ms)\n let nearestSegmentIndex = 0;\n let nearestDistance = Number.MAX_SAFE_INTEGER;\n\n for (let i = 0; i < segments.length; i++) {\n // biome-ignore lint/style/noNonNullAssertion: we know the segment is not null\n const segment = segments[i]!;\n const segmentStartTime = segment.cts;\n const segmentEndTime = segment.cts + segment.duration;\n\n let distance: number;\n if (scaledSeekTime < segmentStartTime) {\n // Time is before this segment\n distance = segmentStartTime - scaledSeekTime;\n } else if (scaledSeekTime >= segmentEndTime) {\n // Time is after this segment\n distance = scaledSeekTime - segmentEndTime;\n } else {\n // Time is within this segment (should have been caught above, but just in case)\n return i;\n }\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestSegmentIndex = i;\n }\n }\n\n return nearestSegmentIndex;\n }\n\n getScrubVideoRendition(): VideoRendition | undefined {\n const scrubTrack = this.scrubTrackIndex;\n\n if (!scrubTrack || scrubTrack.track === undefined) {\n return undefined;\n }\n\n // Calculate segment duration from scrub track segments\n // Scrub tracks use 30-second segments\n const scrubSegmentDurationMs = 30000;\n\n // Calculate segment durations array if segments exist\n const segmentDurationsMs: number[] | undefined =\n scrubTrack.segments.length > 0\n ? scrubTrack.segments.map((segment) => {\n // Convert segment duration from timescale units to milliseconds\n return (segment.duration / scrubTrack.timescale) * 1000;\n })\n : undefined;\n\n return {\n id: \"scrub\" as RenditionId, // Use JIT-style rendition ID\n trackId: scrubTrack.track,\n src: this.src,\n segmentDurationMs: scrubSegmentDurationMs,\n segmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n };\n }\n\n /**\n * Get preferred buffer configuration for this media engine\n * AssetMediaEngine uses lower buffering since segments are already optimized\n */\n getBufferConfig() {\n return {\n // Buffer just 1 segment ahead (~2 seconds) for assets\n videoBufferDurationMs: 2000,\n audioBufferDurationMs: 2000,\n maxVideoBufferFetches: 1,\n maxAudioBufferFetches: 1,\n bufferThresholdMs: 30000, // Timeline-aware buffering threshold\n };\n }\n\n /**\n * Extract thumbnail canvases using main video rendition\n * Note: We prefer main video over scrub track because scrub track in AssetMediaEngine\n * may have incomplete segment data that doesn't cover the full video duration.\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n // Use main video rendition for thumbnails - scrub track may have incomplete segments\n const rendition = this.videoRendition;\n\n if (!rendition) {\n console.warn(\n \"AssetMediaEngine: No video rendition available for thumbnails\",\n );\n return timestamps.map(() => null);\n }\n\n return this.thumbnailExtractor.extractThumbnails(\n timestamps,\n rendition,\n this.durationMs,\n signal,\n );\n }\n\n convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n _segmentId: number,\n rendition: VideoRendition,\n ): number[] {\n // For fragmented MP4 (Asset), when we create a mediabunny Input from init+media segment,\n // mediabunny sees the samples with their ABSOLUTE timestamps from the container.\n // This is because the tfdt box contains the baseMediaDecodeTime which is the absolute\n // position of this segment in the container timeline.\n //\n // So we just need to convert user time to container time by adding startTimeOffsetMs,\n // then pass that to mediabunny (in seconds).\n \n const startTimeOffsetMs = rendition.startTimeOffsetMs || 0;\n\n return globalTimestamps.map((globalMs) => {\n // User time -> container time -> seconds for mediabunny\n const containerTimeMs = globalMs + startTimeOffsetMs;\n return containerTimeMs / 1000;\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -63,10 +63,28 @@ var BaseMediaEngine = class {
|
|
|
63
63
|
const promise = globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
64
64
|
const fetchStart = performance.now();
|
|
65
65
|
try {
|
|
66
|
-
const response = await this.host.fetch(url, {
|
|
66
|
+
const response = await this.host.fetch(url, {
|
|
67
|
+
headers,
|
|
68
|
+
signal
|
|
69
|
+
});
|
|
67
70
|
const fetchEnd = performance.now();
|
|
68
71
|
span.setAttribute("fetchMs", fetchEnd - fetchStart);
|
|
69
|
-
if (
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);
|
|
75
|
+
}
|
|
76
|
+
if (responseType === "json") {
|
|
77
|
+
const contentType = response.headers.get("content-type");
|
|
78
|
+
if (contentType && !contentType.includes("application/json") && !contentType.includes("text/json")) {
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return await response.json();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
const buffer = await response.arrayBuffer();
|
|
71
89
|
span.setAttribute("sizeBytes", buffer.byteLength);
|
|
72
90
|
return buffer;
|
|
@@ -139,10 +157,10 @@ var BaseMediaEngine = class {
|
|
|
139
157
|
* Fetch media segment with built-in deduplication
|
|
140
158
|
* Now uses global deduplication for all requests
|
|
141
159
|
*/
|
|
142
|
-
async fetchMediaSegmentWithDeduplication(segmentId, rendition,
|
|
160
|
+
async fetchMediaSegmentWithDeduplication(segmentId, rendition, signal) {
|
|
143
161
|
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
144
162
|
return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
145
|
-
return this.fetchMediaSegment(segmentId, rendition);
|
|
163
|
+
return this.fetchMediaSegment(segmentId, rendition, signal);
|
|
146
164
|
});
|
|
147
165
|
}
|
|
148
166
|
/**
|
|
@@ -234,7 +252,7 @@ var BaseMediaEngine = class {
|
|
|
234
252
|
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
235
253
|
* Default implementation provides helpful error information
|
|
236
254
|
*/
|
|
237
|
-
async extractThumbnails(timestamps) {
|
|
255
|
+
async extractThumbnails(timestamps, signal) {
|
|
238
256
|
const engineName = this.constructor.name;
|
|
239
257
|
console.warn(`${engineName}: extractThumbnails not properly implemented. This MediaEngine type does not support thumbnail generation. Supported engines: JitMediaEngine. Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? "" : "s"}.`);
|
|
240
258
|
return timestamps.map(() => null);
|
|
@@ -255,5 +273,5 @@ var BaseMediaEngine = class {
|
|
|
255
273
|
};
|
|
256
274
|
|
|
257
275
|
//#endregion
|
|
258
|
-
export { BaseMediaEngine };
|
|
276
|
+
export { BaseMediaEngine, mediaCache };
|
|
259
277
|
//# sourceMappingURL=BaseMediaEngine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n abstract get videoRendition(): VideoRendition | undefined;\n abstract get audioRendition(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined gracefully.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n /**\n * Generate cache key for segment requests\n */\n private getSegmentCacheKey(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined; id?: string },\n ): string {\n return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n // Make the fetch request WITHOUT the signal - let each caller handle their own abort\n // This prevents one instance's abort from affecting other instances using the shared cache\n const response = await this.host.fetch(url, { headers });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n if (responseType === \"json\") {\n return response.json();\n }\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Handle the case where the promise might be aborted\n promise.catch((error) => {\n // If the request was aborted, remove it from cache to prevent corrupted data\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n }),\n ]);\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n // Legacy methods for backward compatibility\n async fetchMediaCache(\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMedia(url, signal);\n }\n\n async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {\n return this.fetchManifest(url, signal);\n }\n\n async fetchMediaCacheWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMediaWithHeaders(url, headers, signal);\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Fetch media segment with built-in deduplication\n * Now uses global deduplication for all requests\n */\n async fetchMediaSegmentWithDeduplication(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n _signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n\n return globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n return this.fetchMediaSegment(segmentId, rendition);\n });\n }\n\n /**\n * Check if a segment is currently being fetched\n */\n isSegmentBeingFetched(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined },\n ): boolean {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n return globalRequestDeduplicator.isPending(cacheKey);\n }\n\n /**\n * Get count of active segment requests (for debugging/monitoring)\n */\n getActiveSegmentRequestCount(): number {\n return globalRequestDeduplicator.getPendingCount();\n }\n\n /**\n * Cancel all active segment requests (for cleanup)\n */\n cancelAllSegmentRequests(): void {\n globalRequestDeduplicator.clear();\n }\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * This needs to check the URL-based cache since that's where segments are actually stored\n */\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n try {\n // Check if this is a JIT engine by looking for urlGenerator property\n const maybeJitEngine = this as any;\n if (\n maybeJitEngine.urlGenerator &&\n typeof maybeJitEngine.urlGenerator.generateSegmentUrl === \"function\"\n ) {\n // This is a JIT engine - generate the URL and check URL-based cache\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n maybeJitEngine,\n );\n const urlIsCached = mediaCache.has(segmentUrl);\n\n return urlIsCached;\n }\n // For other engine types, fall back to the old segment-based key approach\n const cacheKey = `${rendition.src}-${rendition.id || \"default\"}-${segmentId}-${rendition.trackId}`;\n const isCached = mediaCache.has(cacheKey);\n return isCached;\n } catch (error) {\n console.warn(\n `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,\n error,\n );\n return false;\n }\n }\n\n /**\n * Get cached segment IDs from a list for a given rendition\n */\n getCachedSegments(\n segmentIds: number[],\n rendition: AudioRendition | VideoRendition,\n ): Set<number> {\n return new Set(\n segmentIds.filter((id) => this.isSegmentCached(id, rendition)),\n );\n }\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n\n /**\n * Get buffer configuration for this media engine\n * Can be overridden by subclasses to provide custom buffer settings\n */\n getBufferConfig(): {\n videoBufferDurationMs: number;\n audioBufferDurationMs: number;\n maxVideoBufferFetches: number;\n maxAudioBufferFetches: number;\n bufferThresholdMs: number;\n } {\n return {\n videoBufferDurationMs: 10000, // 10 seconds\n audioBufferDurationMs: 10000, // 10 seconds\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BaseMediaEngine.js","names":["result","tEnd","segments: SegmentTimeRange[]"],"sources":["../../../src/elements/EFMedia/BaseMediaEngine.ts"],"sourcesContent":["import { withSpan } from \"../../otel/tracingHelpers.js\";\nimport { RequestDeduplicator } from \"../../transcoding/cache/RequestDeduplicator.js\";\nimport type {\n AudioRendition,\n SegmentTimeRange,\n ThumbnailResult,\n VideoRendition,\n} from \"../../transcoding/types\";\nimport { SizeAwareLRUCache } from \"../../utils/LRUCache.js\";\nimport type { EFMedia } from \"../EFMedia.js\";\nimport type { MediaRendition } from \"./shared/MediaTaskUtils.js\";\n\n// Global instances shared across all media engines\nexport const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit\nexport const globalRequestDeduplicator = new RequestDeduplicator();\n\nexport abstract class BaseMediaEngine {\n protected host: EFMedia;\n\n constructor(host: EFMedia) {\n this.host = host;\n }\n\n abstract get videoRendition(): VideoRendition | undefined;\n abstract get audioRendition(): AudioRendition | undefined;\n\n /**\n * Get video rendition if available. Returns undefined for audio-only assets.\n * Callers should handle undefined gracefully.\n */\n getVideoRendition(): VideoRendition | undefined {\n return this.videoRendition;\n }\n\n /**\n * Get audio rendition if available. Returns undefined for video-only assets.\n * Callers should handle undefined gracefully.\n */\n getAudioRendition(): AudioRendition | undefined {\n return this.audioRendition;\n }\n\n /**\n * Generate cache key for segment requests\n */\n private getSegmentCacheKey(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined; id?: string },\n ): string {\n return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;\n }\n\n /**\n * Unified fetch method with caching and global deduplication\n * All requests (media, manifest, init segments) go through this method\n */\n protected async fetchWithCache(\n url: string,\n options: {\n responseType: \"arrayBuffer\" | \"json\";\n headers?: Record<string, string>;\n signal?: AbortSignal;\n },\n ): Promise<any> {\n return withSpan(\n \"mediaEngine.fetchWithCache\",\n {\n url: url.length > 100 ? `${url.substring(0, 100)}...` : url,\n responseType: options.responseType,\n hasHeaders: !!options.headers,\n },\n undefined,\n async (span) => {\n const t0 = performance.now();\n const { responseType, headers, signal } = options;\n\n // Create cache key that includes URL and headers for proper isolation\n // Note: We don't include signal in cache key as it would prevent proper deduplication\n const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;\n\n // Check cache first\n const t1 = performance.now();\n const cached = mediaCache.get(cacheKey);\n const t2 = performance.now();\n span.setAttribute(\"cacheLookupMs\", Math.round((t2 - t1) * 1000) / 1000);\n\n if (cached) {\n span.setAttribute(\"cacheHit\", true);\n // If we have a cached promise, we need to handle the caller's abort signal\n // without affecting the underlying request that other instances might be using\n if (signal) {\n const t3 = performance.now();\n const result = await this.handleAbortForCachedRequest(\n cached,\n signal,\n );\n const t4 = performance.now();\n span.setAttribute(\n \"handleAbortMs\",\n Math.round((t4 - t3) * 100) / 100,\n );\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t4 - t0) * 100) / 100,\n );\n return result;\n }\n span.setAttribute(\n \"totalCacheHitMs\",\n Math.round((t2 - t0) * 100) / 100,\n );\n return cached;\n }\n\n span.setAttribute(\"cacheHit\", false);\n\n // Use global deduplicator to prevent concurrent requests for the same resource\n // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort\n const promise = globalRequestDeduplicator.executeRequest(\n cacheKey,\n async () => {\n const fetchStart = performance.now();\n try {\n // Pass the signal to host.fetch() so network requests can be canceled when tasks are aborted\n // If multiple callers are waiting on the same request and one aborts, the request will be canceled\n // Other callers will get an error, but they can retry if needed\n const response = await this.host.fetch(url, { headers, signal });\n const fetchEnd = performance.now();\n span.setAttribute(\"fetchMs\", fetchEnd - fetchStart);\n\n // Check response status before parsing\n if (!response.ok) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Failed to fetch: ${response.status} ${text.substring(0, 100)}`);\n }\n\n if (responseType === \"json\") {\n // Check content type header (doesn't consume body)\n const contentType = response.headers.get(\"content-type\");\n if (contentType && !contentType.includes(\"application/json\") && !contentType.includes(\"text/json\")) {\n // Read body once - can't read again after this\n const text = await response.text();\n throw new Error(`Expected JSON but got ${contentType}: ${text.substring(0, 100)}`);\n }\n try {\n // Read body once as JSON\n return await response.json();\n } catch (error) {\n // JSON parse failed - body is already consumed, can't read again\n // The error should contain enough info, but if we need the text, we'd need to clone the response first\n throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n const buffer = await response.arrayBuffer();\n span.setAttribute(\"sizeBytes\", buffer.byteLength);\n return buffer;\n } catch (error) {\n // If the request was aborted, don't cache the error\n if (\n error instanceof DOMException &&\n error.name === \"AbortError\"\n ) {\n // Remove from cache so other requests can retry\n mediaCache.delete(cacheKey);\n }\n throw error;\n }\n },\n );\n\n // Cache the promise (not the result) to handle concurrent requests\n mediaCache.set(cacheKey, promise);\n\n // Handle the case where the promise might be aborted\n promise.catch((error) => {\n // If the request was aborted, remove it from cache to prevent corrupted data\n if (error instanceof DOMException && error.name === \"AbortError\") {\n mediaCache.delete(cacheKey);\n }\n });\n\n // If the caller has a signal, handle abort logic without affecting the underlying request\n if (signal) {\n const result = await this.handleAbortForCachedRequest(\n promise,\n signal,\n );\n const tEnd = performance.now();\n span.setAttribute(\n \"totalFetchMs\",\n Math.round((tEnd - t0) * 100) / 100,\n );\n return result;\n }\n\n const result = await promise;\n const tEnd = performance.now();\n span.setAttribute(\"totalFetchMs\", Math.round((tEnd - t0) * 100) / 100);\n return result;\n },\n );\n }\n\n /**\n * Handles abort logic for a cached request without affecting the underlying fetch\n * This allows multiple instances to share the same cached request while each\n * manages their own abort behavior\n */\n private handleAbortForCachedRequest<T>(\n promise: Promise<T>,\n signal: AbortSignal,\n ): Promise<T> {\n // If signal is already aborted, reject immediately\n if (signal.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Return a promise that respects the caller's abort signal\n // but doesn't affect the underlying cached request\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n signal.addEventListener(\"abort\", () => {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n });\n }),\n ]);\n }\n\n // Public wrapper methods that delegate to fetchWithCache\n async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"arrayBuffer\", signal });\n }\n\n async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, { responseType: \"json\", signal });\n }\n\n async fetchMediaWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n // Check abort signal immediately before any processing\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n return this.fetchWithCache(url, {\n responseType: \"arrayBuffer\",\n headers,\n signal,\n });\n }\n\n // Legacy methods for backward compatibility\n async fetchMediaCache(\n url: string,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMedia(url, signal);\n }\n\n async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {\n return this.fetchManifest(url, signal);\n }\n\n async fetchMediaCacheWithHeaders(\n url: string,\n headers: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n return this.fetchMediaWithHeaders(url, headers, signal);\n }\n\n /**\n * Abstract method for actual segment fetching - implemented by subclasses\n */\n abstract fetchMediaSegment(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract fetchInitSegment(\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer>;\n\n abstract computeSegmentId(\n desiredSeekTimeMs: number,\n rendition: MediaRendition,\n ): number | undefined;\n\n /**\n * Fetch media segment with built-in deduplication\n * Now uses global deduplication for all requests\n */\n async fetchMediaSegmentWithDeduplication(\n segmentId: number,\n rendition: { trackId: number | undefined; src: string },\n signal?: AbortSignal,\n ): Promise<ArrayBuffer> {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n\n return globalRequestDeduplicator.executeRequest(cacheKey, async () => {\n return this.fetchMediaSegment(segmentId, rendition, signal);\n });\n }\n\n /**\n * Check if a segment is currently being fetched\n */\n isSegmentBeingFetched(\n segmentId: number,\n rendition: { src: string; trackId: number | undefined },\n ): boolean {\n const cacheKey = this.getSegmentCacheKey(segmentId, rendition);\n return globalRequestDeduplicator.isPending(cacheKey);\n }\n\n /**\n * Get count of active segment requests (for debugging/monitoring)\n */\n getActiveSegmentRequestCount(): number {\n return globalRequestDeduplicator.getPendingCount();\n }\n\n /**\n * Cancel all active segment requests (for cleanup)\n */\n cancelAllSegmentRequests(): void {\n globalRequestDeduplicator.clear();\n }\n\n /**\n * Calculate audio segments needed for a time range\n * Each media engine implements this based on their segment structure\n */\n calculateAudioSegmentRange(\n fromMs: number,\n toMs: number,\n rendition: AudioRendition,\n durationMs: number,\n ): SegmentTimeRange[] {\n // Default implementation for uniform segments (used by JitMediaEngine)\n if (fromMs >= toMs) {\n return [];\n }\n\n const segments: SegmentTimeRange[] = [];\n\n // Use actual segment durations if available (more accurate)\n if (\n rendition.segmentDurationsMs &&\n rendition.segmentDurationsMs.length > 0\n ) {\n let cumulativeTime = 0;\n\n for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {\n const segmentDuration = rendition.segmentDurationsMs[i];\n if (segmentDuration === undefined) {\n continue; // Skip undefined segment durations\n }\n const segmentStartMs = cumulativeTime;\n const segmentEndMs = Math.min(\n cumulativeTime + segmentDuration,\n durationMs,\n );\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId: i + 1, // Convert to 1-based\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n\n cumulativeTime += segmentDuration;\n\n // If we've reached or exceeded file duration, stop\n if (cumulativeTime >= durationMs) {\n break;\n }\n }\n\n return segments;\n }\n\n // Fall back to fixed duration calculation for backward compatibility\n const segmentDurationMs = rendition.segmentDurationMs || 1000;\n const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(toMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = i + 1; // Convert to 1-based\n const segmentStartMs = i * segmentDurationMs;\n const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);\n\n // Don't include segments that start at or beyond the file duration\n if (segmentStartMs >= durationMs) {\n break;\n }\n\n // Only include segments that overlap with requested time range\n if (segmentStartMs < toMs && segmentEndMs > fromMs) {\n segments.push({\n segmentId,\n startMs: segmentStartMs,\n endMs: segmentEndMs,\n });\n }\n }\n\n return segments;\n }\n\n /**\n * Check if a segment is cached for a given rendition\n * This needs to check the URL-based cache since that's where segments are actually stored\n */\n isSegmentCached(\n segmentId: number,\n rendition: AudioRendition | VideoRendition,\n ): boolean {\n try {\n // Check if this is a JIT engine by looking for urlGenerator property\n const maybeJitEngine = this as any;\n if (\n maybeJitEngine.urlGenerator &&\n typeof maybeJitEngine.urlGenerator.generateSegmentUrl === \"function\"\n ) {\n // This is a JIT engine - generate the URL and check URL-based cache\n if (!rendition.id) {\n return false;\n }\n\n const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(\n segmentId,\n rendition.id,\n maybeJitEngine,\n );\n const urlIsCached = mediaCache.has(segmentUrl);\n\n return urlIsCached;\n }\n // For other engine types, fall back to the old segment-based key approach\n const cacheKey = `${rendition.src}-${rendition.id || \"default\"}-${segmentId}-${rendition.trackId}`;\n const isCached = mediaCache.has(cacheKey);\n return isCached;\n } catch (error) {\n console.warn(\n `🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,\n error,\n );\n return false;\n }\n }\n\n /**\n * Get cached segment IDs from a list for a given rendition\n */\n getCachedSegments(\n segmentIds: number[],\n rendition: AudioRendition | VideoRendition,\n ): Set<number> {\n return new Set(\n segmentIds.filter((id) => this.isSegmentCached(id, rendition)),\n );\n }\n\n /**\n * Extract thumbnail canvases at multiple timestamps efficiently\n * Default implementation provides helpful error information\n */\n async extractThumbnails(\n timestamps: number[],\n signal?: AbortSignal,\n ): Promise<(ThumbnailResult | null)[]> {\n const engineName = this.constructor.name;\n console.warn(\n `${engineName}: extractThumbnails not properly implemented. ` +\n \"This MediaEngine type does not support thumbnail generation. \" +\n \"Supported engines: JitMediaEngine. \" +\n `Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? \"\" : \"s\"}.`,\n );\n return timestamps.map(() => null);\n }\n\n abstract convertToSegmentRelativeTimestamps(\n globalTimestamps: number[],\n segmentId: number,\n rendition: VideoRendition,\n ): number[];\n\n /**\n * Get buffer configuration for this media engine\n * Can be overridden by subclasses to provide custom buffer settings\n */\n getBufferConfig(): {\n videoBufferDurationMs: number;\n audioBufferDurationMs: number;\n maxVideoBufferFetches: number;\n maxAudioBufferFetches: number;\n bufferThresholdMs: number;\n } {\n return {\n videoBufferDurationMs: 10000, // 10 seconds\n audioBufferDurationMs: 10000, // 10 seconds\n maxVideoBufferFetches: 3,\n maxAudioBufferFetches: 3,\n bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,9 +3,11 @@ import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/elements/EFMedia/JitMediaEngine.ts
|
|
5
5
|
var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
6
|
-
static async fetch(host, urlGenerator, url) {
|
|
6
|
+
static async fetch(host, urlGenerator, url, signal) {
|
|
7
7
|
const engine = new JitMediaEngine(host, urlGenerator);
|
|
8
|
-
|
|
8
|
+
const data = await engine.fetchManifest(url, signal);
|
|
9
|
+
signal?.throwIfAborted();
|
|
10
|
+
engine.data = data;
|
|
9
11
|
return engine;
|
|
10
12
|
}
|
|
11
13
|
constructor(host, urlGenerator) {
|
|
@@ -29,7 +31,8 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
29
31
|
trackId: void 0,
|
|
30
32
|
src: this.data.sourceUrl,
|
|
31
33
|
segmentDurationMs: rendition.segmentDurationMs,
|
|
32
|
-
segmentDurationsMs: rendition.segmentDurationsMs
|
|
34
|
+
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
35
|
+
startTimeOffsetMs: rendition.startTimeOffsetMs
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
get videoRendition() {
|
|
@@ -41,7 +44,8 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
41
44
|
trackId: void 0,
|
|
42
45
|
src: this.data.sourceUrl,
|
|
43
46
|
segmentDurationMs: rendition.segmentDurationMs,
|
|
44
|
-
segmentDurationsMs: rendition.segmentDurationsMs
|
|
47
|
+
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
48
|
+
startTimeOffsetMs: rendition.startTimeOffsetMs
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
get templates() {
|
|
@@ -52,10 +56,10 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
52
56
|
const url = this.urlGenerator.generateSegmentUrl("init", rendition.id, this);
|
|
53
57
|
return this.fetchMedia(url, signal);
|
|
54
58
|
}
|
|
55
|
-
async fetchMediaSegment(segmentId, rendition) {
|
|
59
|
+
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
56
60
|
if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
|
|
57
61
|
const url = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
|
|
58
|
-
return this.fetchMedia(url);
|
|
62
|
+
return this.fetchMedia(url, signal);
|
|
59
63
|
}
|
|
60
64
|
computeSegmentId(desiredSeekTimeMs, rendition) {
|
|
61
65
|
if (desiredSeekTimeMs > this.durationMs) return;
|
|
@@ -106,7 +110,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
106
110
|
/**
|
|
107
111
|
* Extract thumbnail canvases using same rendition priority as video playback for frame alignment
|
|
108
112
|
*/
|
|
109
|
-
async extractThumbnails(timestamps) {
|
|
113
|
+
async extractThumbnails(timestamps, signal) {
|
|
110
114
|
let rendition;
|
|
111
115
|
try {
|
|
112
116
|
const mainRendition = this.getVideoRendition();
|
|
@@ -120,7 +124,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
120
124
|
console.warn("JitMediaEngine: No video rendition available for thumbnails", error);
|
|
121
125
|
return timestamps.map(() => null);
|
|
122
126
|
}
|
|
123
|
-
return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs);
|
|
127
|
+
return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs, signal);
|
|
124
128
|
}
|
|
125
129
|
convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, _rendition) {
|
|
126
130
|
return globalTimestamps.map((timestamp) => timestamp / 1e3);
|