@editframe/elements 0.33.0-beta → 0.34.6-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.js +5 -3
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.94.0 → _@oxc-project_runtime@0.95.0}/helpers/decorate.js +1 -1
- package/dist/canvas/EFCanvas.d.ts +7 -4
- package/dist/canvas/EFCanvas.js +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +4 -4
- package/dist/canvas/EFCanvasItem.js +1 -1
- package/dist/canvas/overlays/SelectionOverlay.d.ts +95 -0
- package/dist/canvas/overlays/SelectionOverlay.js +1 -1
- package/dist/canvas/selection/SelectionController.js +7 -11
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +25 -7
- package/dist/elements/EFAudio.js +31 -61
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +65 -52
- package/dist/elements/EFCaptions.js +186 -400
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +34 -6
- package/dist/elements/EFImage.js +114 -79
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +17 -17
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +41 -25
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +4 -4
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +31 -17
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +17 -9
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +66 -20
- package/dist/elements/EFMedia.js +412 -30
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +4 -4
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFSourceMixin.js +43 -15
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +23 -10
- package/dist/elements/EFSurface.js +64 -22
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +8 -2
- package/dist/elements/EFTemporal.js +42 -31
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +11 -2
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTextSegment.js +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +4 -4
- package/dist/elements/EFThumbnailStrip.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +22 -8
- package/dist/elements/EFTimegroup.js +203 -115
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +57 -20
- package/dist/elements/EFVideo.js +324 -72
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +33 -7
- package/dist/elements/EFWaveform.js +103 -59
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js +14 -3
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +1 -1
- package/dist/gui/Controllable.d.ts +2 -0
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFConfiguration.js +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +1 -1
- package/dist/gui/EFFilmstrip.d.ts +3 -2
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.js +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFFocusOverlay.js +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +4 -4
- package/dist/gui/EFTransformHandles.js +1 -1
- package/dist/gui/EFWorkbench.d.ts +5 -4
- package/dist/gui/EFWorkbench.js +1 -1
- package/dist/gui/PlaybackController.d.ts +10 -2
- package/dist/gui/PlaybackController.js +52 -30
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +6 -2
- package/dist/gui/timeline/EFTimeline.js +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +57 -0
- package/dist/gui/timeline/EFTimelineRow.js +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.d.ts +2 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +58 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +1 -1
- package/dist/gui/timeline/tracks/HTMLTrack.d.ts +13 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +26 -0
- package/dist/gui/timeline/tracks/TextTrack.js +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +47 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +4 -12
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +81 -0
- package/dist/gui/timeline/tracks/TrackItem.js +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +25 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +1 -1
- package/dist/gui/timeline/tracks/ensureTrackItemInit.d.ts +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.d.ts +9 -0
- package/dist/gui/tree/EFTree.d.ts +5 -4
- package/dist/gui/tree/EFTree.js +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/preview/AdaptiveResolutionTracker.js +6 -14
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +123 -0
- package/dist/preview/FrameController.js +216 -0
- package/dist/preview/FrameController.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +1 -0
- package/dist/preview/RenderContext.js +193 -0
- package/dist/preview/RenderContext.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +166 -0
- package/dist/preview/encoding/canvasEncoder.js.map +1 -0
- package/dist/preview/encoding/mainThreadEncoder.js +39 -0
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -0
- package/dist/preview/encoding/types.d.ts +1 -0
- package/dist/preview/encoding/workerEncoder.js +58 -0
- package/dist/preview/encoding/workerEncoder.js.map +1 -0
- package/dist/preview/logger.js +41 -0
- package/dist/preview/logger.js.map +1 -0
- package/dist/preview/previewTypes.js +11 -10
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +259 -236
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +5 -0
- package/dist/preview/renderTimegroupToCanvas.js +100 -489
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.d.ts +1 -0
- package/dist/preview/renderTimegroupToVideo.js +80 -22
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/inlineImages.js +56 -0
- package/dist/preview/rendering/inlineImages.js.map +1 -0
- package/dist/preview/rendering/renderToImage.d.ts +1 -0
- package/dist/preview/rendering/renderToImage.js +120 -0
- package/dist/preview/rendering/renderToImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageForeignObject.js +135 -0
- package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.d.ts +1 -0
- package/dist/preview/rendering/renderToImageNative.js +129 -0
- package/dist/preview/rendering/renderToImageNative.js.map +1 -0
- package/dist/preview/rendering/svgSerializer.js +43 -0
- package/dist/preview/rendering/svgSerializer.js.map +1 -0
- package/dist/preview/rendering/types.d.ts +2 -0
- package/dist/preview/statsTrackingStrategy.js +3 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +8 -57
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +35 -0
- package/dist/render/EFRenderAPI.js +1 -0
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/sandbox/PlaybackControls.d.ts +1 -0
- package/dist/sandbox/ScenarioRunner.d.ts +1 -0
- package/dist/sandbox/defineSandbox.d.ts +1 -0
- package/dist/sandbox/index.d.ts +3 -0
- package/dist/style.css +3 -0
- package/dist/transcoding/types/index.d.ts +6 -3
- package/package.json +2 -3
- package/test/EFVideo.framegen.browsertest.ts +8 -1
- package/test/profilingPlugin.ts +1 -3
- package/test/setup.ts +23 -1
- package/dist/EF_INTERACTIVE.js +0 -7
- package/dist/EF_INTERACTIVE.js.map +0 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +0 -50
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +0 -104
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -168
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +0 -46
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +0 -30
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +0 -47
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -140
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +0 -13
- package/dist/elements/EFMedia/shared/BufferUtils.js +0 -86
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +0 -1
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +0 -90
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +0 -80
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -58
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +0 -71
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -52
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -50
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +0 -109
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +0 -97
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +0 -1
- package/dist/elements/SampleBuffer.d.ts +0 -19
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
2
|
-
import { LRUCache } from "../../../utils/LRUCache.js";
|
|
3
|
-
import { IgnorableError } from "../../EFMedia.js";
|
|
4
|
-
import { Task } from "@lit/task";
|
|
5
|
-
|
|
6
|
-
//#region src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts
|
|
7
|
-
const DECAY_WEIGHT = .8;
|
|
8
|
-
function makeAudioTimeDomainAnalysisTask(element) {
|
|
9
|
-
const cache = new LRUCache(1e3);
|
|
10
|
-
let task;
|
|
11
|
-
task = new Task(element, {
|
|
12
|
-
autoRun: EF_INTERACTIVE,
|
|
13
|
-
onError: (error) => {
|
|
14
|
-
task.taskComplete.catch(() => {});
|
|
15
|
-
if (error instanceof IgnorableError) {
|
|
16
|
-
console.info("byteTimeDomainTask skipped: no audio track");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
|
|
20
|
-
if (error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("is not valid JSON") || error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch"))) return;
|
|
21
|
-
console.error("byteTimeDomainTask error", error);
|
|
22
|
-
},
|
|
23
|
-
args: () => [element.currentSourceTimeMs],
|
|
24
|
-
task: async (_, { signal }) => {
|
|
25
|
-
if (element.currentSourceTimeMs < 0) return null;
|
|
26
|
-
const currentTimeMs = element.currentSourceTimeMs;
|
|
27
|
-
const frameIntervalMs = 1e3 / 30;
|
|
28
|
-
const earliestFrameMs = currentTimeMs - (element.fftDecay - 1) * frameIntervalMs;
|
|
29
|
-
const fromMs = Math.max(0, earliestFrameMs);
|
|
30
|
-
const maxToMs = currentTimeMs + frameIntervalMs;
|
|
31
|
-
const videoDurationMs = element.intrinsicDurationMs || 0;
|
|
32
|
-
const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
|
|
33
|
-
if (fromMs >= toMs) return null;
|
|
34
|
-
const preliminaryCacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftDecay}:${element.fftGain}:${fromMs}:${currentTimeMs}`;
|
|
35
|
-
const cachedData = cache.get(preliminaryCacheKey);
|
|
36
|
-
if (cachedData) return cachedData;
|
|
37
|
-
if (element.mediaEngineTask.error) return null;
|
|
38
|
-
let mediaEngine;
|
|
39
|
-
try {
|
|
40
|
-
mediaEngine = await element.mediaEngineTask.taskComplete;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
if (error instanceof Error && error.message === "No valid media source") return null;
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
signal?.throwIfAborted();
|
|
46
|
-
if (!mediaEngine?.audioRendition) return null;
|
|
47
|
-
if (element.audioInputTask.error) return null;
|
|
48
|
-
if (element.audioInputTask.value === void 0) return null;
|
|
49
|
-
const { fetchAudioSpanningTime: fetchAudioSpan } = await import("../shared/AudioSpanUtils.js");
|
|
50
|
-
try {
|
|
51
|
-
const audioSpan = await fetchAudioSpan(element, fromMs, toMs, signal);
|
|
52
|
-
if (!audioSpan || !audioSpan.blob) return null;
|
|
53
|
-
if (audioSpan.blob.size < 100) return null;
|
|
54
|
-
const tempAudioContext = new OfflineAudioContext(2, 48e3, 48e3);
|
|
55
|
-
const arrayBuffer = await audioSpan.blob.arrayBuffer();
|
|
56
|
-
signal?.throwIfAborted();
|
|
57
|
-
if (arrayBuffer.byteLength < 100) return null;
|
|
58
|
-
let audioBuffer;
|
|
59
|
-
try {
|
|
60
|
-
audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);
|
|
61
|
-
signal?.throwIfAborted();
|
|
62
|
-
} catch (decodeError) {
|
|
63
|
-
if (decodeError instanceof Error && decodeError.message.includes("Unable to decode audio data")) return null;
|
|
64
|
-
throw decodeError;
|
|
65
|
-
}
|
|
66
|
-
const startOffsetMs = audioSpan.startMs;
|
|
67
|
-
const framesData = await Promise.all(Array.from({ length: element.fftDecay }, async (_$1, frameIndex) => {
|
|
68
|
-
const frameOffset = frameIndex * (1e3 / 30);
|
|
69
|
-
const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1e3);
|
|
70
|
-
const cacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftGain}:${startOffsetMs}:${startTime}`;
|
|
71
|
-
const cachedFrame = cache.get(cacheKey);
|
|
72
|
-
if (cachedFrame) return cachedFrame;
|
|
73
|
-
let audioContext;
|
|
74
|
-
try {
|
|
75
|
-
audioContext = new OfflineAudioContext(2, 48e3 * (1 / 30), 48e3);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
throw new Error(`[EFMedia.byteTimeDomainTask] Failed to create OfflineAudioContext(2, ${48e3 * (1 / 30)}, 48000) for frame ${frameIndex} at time ${startTime}s: ${error instanceof Error ? error.message : String(error)}. This is for audio time domain analysis.`);
|
|
78
|
-
}
|
|
79
|
-
const source = audioContext.createBufferSource();
|
|
80
|
-
source.buffer = audioBuffer;
|
|
81
|
-
const analyser = audioContext.createAnalyser();
|
|
82
|
-
analyser.fftSize = element.fftSize;
|
|
83
|
-
analyser.minDecibels = -90;
|
|
84
|
-
analyser.maxDecibels = -20;
|
|
85
|
-
const gainNode = audioContext.createGain();
|
|
86
|
-
gainNode.gain.value = element.fftGain;
|
|
87
|
-
source.connect(gainNode);
|
|
88
|
-
gainNode.connect(analyser);
|
|
89
|
-
analyser.connect(audioContext.destination);
|
|
90
|
-
source.start(0, startTime, 1 / 30);
|
|
91
|
-
const dataLength = analyser.fftSize / 2;
|
|
92
|
-
try {
|
|
93
|
-
await audioContext.startRendering();
|
|
94
|
-
signal?.throwIfAborted();
|
|
95
|
-
const frameData = new Uint8Array(dataLength);
|
|
96
|
-
analyser.getByteTimeDomainData(frameData);
|
|
97
|
-
const points = new Uint8Array(dataLength);
|
|
98
|
-
for (let i = 0; i < dataLength; i++) {
|
|
99
|
-
const pointSamples = frameData.slice(i * (frameData.length / dataLength), (i + 1) * (frameData.length / dataLength));
|
|
100
|
-
const rms = Math.sqrt(pointSamples.reduce((sum, sample) => {
|
|
101
|
-
const normalized = (sample - 128) / 128;
|
|
102
|
-
return sum + normalized * normalized;
|
|
103
|
-
}, 0) / pointSamples.length);
|
|
104
|
-
const avgSign = Math.sign(pointSamples.reduce((sum, sample) => sum + (sample - 128), 0));
|
|
105
|
-
points[i] = Math.min(255, Math.round(128 + avgSign * rms * 128));
|
|
106
|
-
}
|
|
107
|
-
cache.set(cacheKey, points);
|
|
108
|
-
return points;
|
|
109
|
-
} finally {
|
|
110
|
-
source.disconnect();
|
|
111
|
-
analyser.disconnect();
|
|
112
|
-
}
|
|
113
|
-
}));
|
|
114
|
-
const frameLength = framesData[0]?.length ?? 0;
|
|
115
|
-
const smoothedData = new Uint8Array(frameLength);
|
|
116
|
-
for (let i = 0; i < frameLength; i++) {
|
|
117
|
-
let weightedSum = 0;
|
|
118
|
-
let weightSum = 0;
|
|
119
|
-
framesData.forEach((frame, frameIndex) => {
|
|
120
|
-
const decayWeight = DECAY_WEIGHT ** frameIndex;
|
|
121
|
-
weightedSum += (frame[i] ?? 0) * decayWeight;
|
|
122
|
-
weightSum += decayWeight;
|
|
123
|
-
});
|
|
124
|
-
smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
|
|
125
|
-
}
|
|
126
|
-
cache.set(preliminaryCacheKey, smoothedData);
|
|
127
|
-
return smoothedData;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
130
|
-
if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") || error.message.includes("File not found") || error.message.includes("Media segment not found") || error.message.includes("No segments found"))) return null;
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return task;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
//#endregion
|
|
139
|
-
export { makeAudioTimeDomainAnalysisTask };
|
|
140
|
-
//# sourceMappingURL=makeAudioTimeDomainAnalysisTask.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeAudioTimeDomainAnalysisTask.js","names":["task: Task<readonly [number], Uint8Array | null>","audioContext: OfflineAudioContext"],"sources":["../../../../src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\n\nimport { EF_INTERACTIVE } from \"../../../EF_INTERACTIVE.js\";\nimport { LRUCache } from \"../../../utils/LRUCache.js\";\nimport { type EFMedia, IgnorableError } from \"../../EFMedia.js\";\n\n// DECAY_WEIGHT constant - same as original\nconst DECAY_WEIGHT = 0.8;\n\nexport function makeAudioTimeDomainAnalysisTask(element: EFMedia): Task<readonly [number], Uint8Array | null> {\n // Internal cache for this task instance (same as original #byteTimeDomainCache)\n const cache = new LRUCache<string, Uint8Array>(1000);\n\n // Capture task reference for use in onError\n let task: Task<readonly [number], Uint8Array | null>;\n\n task = new Task(element, {\n autoRun: EF_INTERACTIVE,\n onError: (error) => {\n // CRITICAL: Attach .catch() handler to taskComplete BEFORE the promise is rejected.\n // This prevents unhandled rejection when hostUpdate() triggers _performTask() without awaiting.\n task.taskComplete.catch(() => {});\n \n if (error instanceof IgnorableError) {\n console.info(\"byteTimeDomainTask skipped: no audio track\");\n return;\n }\n \n // Don't log AbortErrors - these are expected when tasks are cancelled\n const isAbortError = \n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message?.includes(\"signal is aborted\") ||\n error.message?.includes(\"The user aborted a request\")\n ));\n \n if (isAbortError) {\n return;\n }\n \n // Don't log errors when there's no valid media source, file not found, or auth errors - these are expected\n if (error instanceof Error && (\n error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"is not valid JSON\") ||\n error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n error.message.includes(\"Failed to fetch\")\n )) {\n return;\n }\n console.error(\"byteTimeDomainTask error\", error);\n },\n args: () =>\n [\n element.currentSourceTimeMs,\n ] as const,\n task: async (_, { signal }) => {\n if (element.currentSourceTimeMs < 0) return null;\n\n const currentTimeMs = element.currentSourceTimeMs;\n\n // Calculate exact audio window needed based on fftDecay and frame timing\n const frameIntervalMs = 1000 / 30; // 33.33ms per frame\n\n // Need audio from earliest frame to current frame\n const earliestFrameMs =\n currentTimeMs - (element.fftDecay - 1) * frameIntervalMs;\n const fromMs = Math.max(0, earliestFrameMs);\n const maxToMs = currentTimeMs + frameIntervalMs; // Include current frame\n const videoDurationMs = element.intrinsicDurationMs || 0;\n const toMs =\n videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;\n\n // If the clamping results in an invalid range (seeking beyond the end), skip analysis silently\n if (fromMs >= toMs) {\n return null;\n }\n\n // Check cache early - before expensive audio fetching\n // Use a preliminary cache key that doesn't depend on actual startOffsetMs from audio span\n const preliminaryCacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftDecay}:${element.fftGain}:${fromMs}:${currentTimeMs}`;\n const cachedData = cache.get(preliminaryCacheKey);\n if (cachedData) {\n return cachedData;\n }\n\n // Check if media engine task has errored (no valid source) before attempting to use it\n if (element.mediaEngineTask.error) {\n return null;\n }\n \n // Check if audio rendition exists before attempting to fetch audio data\n // This prevents unnecessary HTTP requests and warnings when audio is not available\n let mediaEngine;\n try {\n mediaEngine = await element.mediaEngineTask.taskComplete;\n } catch (error) {\n // If media engine task failed (no valid source), return null silently\n if (error instanceof Error && error.message === \"No valid media source\") {\n return null;\n }\n // Re-throw unexpected errors\n throw error;\n }\n \n // Check for abort after awaiting media engine\n signal?.throwIfAborted();\n \n if (!mediaEngine?.audioRendition) {\n // No audio rendition available - skip silently (no warning needed)\n return null;\n }\n\n // Check if audioInputTask has errored or returned undefined before fetching\n // This prevents fetch calls when we know they'll fail (e.g., 401 auth required)\n if (element.audioInputTask.error) {\n return null;\n }\n const audioInputValue = element.audioInputTask.value;\n if (audioInputValue === undefined) {\n // Audio input is not available - don't try to fetch\n return null;\n }\n\n const { fetchAudioSpanningTime: fetchAudioSpan } =\n await import(\"../shared/AudioSpanUtils.js\");\n \n // Try to fetch audio span, but return null if it fails with expected errors\n try {\n const audioSpan = await fetchAudioSpan(element, fromMs, toMs, signal);\n\n if (!audioSpan || !audioSpan.blob) {\n // Audio data not available - skip silently (already checked for rendition above)\n return null;\n }\n\n // Validate blob has sufficient data before attempting decode\n // Empty or very small blobs will fail decodeAudioData\n if (audioSpan.blob.size < 100) {\n // Too small to be valid audio data - skip silently\n return null;\n }\n\n // Decode the real audio data\n const tempAudioContext = new OfflineAudioContext(2, 48000, 48000);\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n \n // Check for abort after expensive arrayBuffer operation\n signal?.throwIfAborted();\n \n // Validate arrayBuffer before decode attempt\n if (arrayBuffer.byteLength < 100) {\n return null;\n }\n \n let audioBuffer;\n try {\n audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);\n \n // Check for abort after expensive decode operation\n signal?.throwIfAborted();\n } catch (decodeError) {\n // Unable to decode audio data - this means the data isn't valid audio\n // This can happen with corrupted/incomplete segments - skip silently\n if (decodeError instanceof Error && \n decodeError.message.includes(\"Unable to decode audio data\")) {\n return null;\n }\n throw decodeError;\n }\n\n // Use actual startOffset from audioSpan (relative to requested time)\n const startOffsetMs = audioSpan.startMs;\n\n // Process multiple frames with decay, similar to the reference code\n const framesData = await Promise.all(\n Array.from({ length: element.fftDecay }, async (_, frameIndex) => {\n const frameOffset = frameIndex * (1000 / 30);\n const startTime = Math.max(\n 0,\n (currentTimeMs - frameOffset - startOffsetMs) / 1000,\n );\n\n const cacheKey = `${element.shouldInterpolateFrequencies}:${element.fftSize}:${element.fftGain}:${startOffsetMs}:${startTime}`;\n const cachedFrame = cache.get(cacheKey);\n if (cachedFrame) {\n return cachedFrame;\n }\n\n let audioContext: OfflineAudioContext;\n try {\n audioContext = new OfflineAudioContext(2, 48000 * (1 / 30), 48000);\n } catch (error) {\n throw new Error(\n `[EFMedia.byteTimeDomainTask] Failed to create OfflineAudioContext(2, ${48000 * (1 / 30)}, 48000) for frame ${frameIndex} at time ${startTime}s: ${error instanceof Error ? error.message : String(error)}. This is for audio time domain analysis.`,\n );\n }\n\n const source = audioContext.createBufferSource();\n source.buffer = audioBuffer;\n\n // Create analyzer for PCM data\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = element.fftSize; // Ensure enough samples\n analyser.minDecibels = -90;\n analyser.maxDecibels = -20;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = element.fftGain; // Amplify the signal\n\n source.connect(gainNode);\n gainNode.connect(analyser);\n analyser.connect(audioContext.destination);\n\n source.start(0, startTime, 1 / 30);\n\n const dataLength = analyser.fftSize / 2;\n try {\n await audioContext.startRendering();\n \n // Check for abort after expensive rendering operation\n signal?.throwIfAborted();\n \n const frameData = new Uint8Array(dataLength);\n analyser.getByteTimeDomainData(frameData);\n\n // const points = frameData;\n // Calculate RMS and midpoint values\n const points = new Uint8Array(dataLength);\n for (let i = 0; i < dataLength; i++) {\n const pointSamples = frameData.slice(\n i * (frameData.length / dataLength),\n (i + 1) * (frameData.length / dataLength),\n );\n\n // Calculate RMS while preserving sign\n const rms = Math.sqrt(\n pointSamples.reduce((sum, sample) => {\n const normalized = (sample - 128) / 128;\n return sum + normalized * normalized;\n }, 0) / pointSamples.length,\n );\n\n // Get average sign of the samples to determine direction\n const avgSign = Math.sign(\n pointSamples.reduce((sum, sample) => sum + (sample - 128), 0),\n );\n\n // Convert RMS back to byte range, preserving direction\n points[i] = Math.min(255, Math.round(128 + avgSign * rms * 128));\n }\n\n cache.set(cacheKey, points);\n return points;\n } finally {\n source.disconnect();\n analyser.disconnect();\n }\n }),\n );\n\n // Combine frames with decay weighting\n const frameLength = framesData[0]?.length ?? 0;\n const smoothedData = new Uint8Array(frameLength);\n\n for (let i = 0; i < frameLength; i++) {\n let weightedSum = 0;\n let weightSum = 0;\n\n framesData.forEach((frame: Uint8Array, frameIndex: number) => {\n const decayWeight = DECAY_WEIGHT ** frameIndex;\n weightedSum += (frame[i] ?? 0) * decayWeight;\n weightSum += decayWeight;\n });\n\n smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));\n }\n\n // Cache with the preliminary key so future requests can skip audio fetching\n cache.set(preliminaryCacheKey, smoothedData);\n return smoothedData;\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 expected errors (401, missing segments, etc.), return null gracefully\n if (\n error instanceof Error &&\n (error.message.includes(\"401\") ||\n error.message.includes(\"UNAUTHORIZED\") ||\n error.message.includes(\"Failed to fetch\") ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"Media segment not found\") ||\n error.message.includes(\"No segments found\"))\n ) {\n return null;\n }\n // Re-throw unexpected errors\n throw error;\n }\n },\n });\n\n return task;\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/elements/EFMedia/shared/BufferUtils.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* State interface for media buffering - orchestration only, no data storage
|
|
4
|
-
*/
|
|
5
|
-
interface MediaBufferState {
|
|
6
|
-
currentSeekTimeMs: number;
|
|
7
|
-
requestedSegments: Set<number>;
|
|
8
|
-
activeRequests: Set<number>;
|
|
9
|
-
requestQueue: number[];
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { MediaBufferState };
|
|
13
|
-
//# sourceMappingURL=BufferUtils.d.ts.map
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
//#region src/elements/EFMedia/shared/BufferUtils.ts
|
|
2
|
-
/**
|
|
3
|
-
* Async version of computeSegmentRange for when computeSegmentId is async
|
|
4
|
-
*/
|
|
5
|
-
const computeSegmentRangeAsync = async (startTimeMs, endTimeMs, durationMs, rendition, computeSegmentId) => {
|
|
6
|
-
const segments = [];
|
|
7
|
-
const segmentDurationMs = rendition.segmentDurationMs || 1e3;
|
|
8
|
-
const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);
|
|
9
|
-
const endSegmentIndex = Math.floor(Math.min(endTimeMs, durationMs) / segmentDurationMs);
|
|
10
|
-
for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
|
|
11
|
-
const timeMs = i * segmentDurationMs;
|
|
12
|
-
if (timeMs < durationMs) {
|
|
13
|
-
const segmentId = await computeSegmentId(timeMs, rendition);
|
|
14
|
-
if (segmentId !== void 0) segments.push(segmentId);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return segments.filter((id, index, arr) => arr.indexOf(id) === index);
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Compute buffer queue based on desired segments and what we've already requested
|
|
21
|
-
* Pure function - determines what new segments should be prefetched
|
|
22
|
-
*/
|
|
23
|
-
const computeBufferQueue = (desiredSegments, requestedSegments) => {
|
|
24
|
-
return desiredSegments.filter((segmentId) => !requestedSegments.has(segmentId));
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Calculate distance from element to playhead position
|
|
28
|
-
* Returns 0 if element is currently active, otherwise returns distance in milliseconds
|
|
29
|
-
*/
|
|
30
|
-
const calculatePlayheadDistance = (element, playheadMs) => {
|
|
31
|
-
if (playheadMs < element.startTimeMs) return element.startTimeMs - playheadMs;
|
|
32
|
-
if (playheadMs > element.endTimeMs) return playheadMs - element.endTimeMs;
|
|
33
|
-
return 0;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Core media buffering orchestration logic - prefetch only, no data storage
|
|
37
|
-
* Integrates with BaseMediaEngine's existing caching and request deduplication
|
|
38
|
-
*/
|
|
39
|
-
const manageMediaBuffer = async (seekTimeMs, config, currentState, durationMs, signal, deps, timelineContext) => {
|
|
40
|
-
if (!config.enableBuffering) return currentState;
|
|
41
|
-
if (timelineContext && config.bufferThresholdMs !== void 0) {
|
|
42
|
-
if (calculatePlayheadDistance({
|
|
43
|
-
startTimeMs: timelineContext.elementStartMs,
|
|
44
|
-
endTimeMs: timelineContext.elementEndMs
|
|
45
|
-
}, timelineContext.playheadMs) > config.bufferThresholdMs) return currentState;
|
|
46
|
-
}
|
|
47
|
-
const rendition = await deps.getRendition();
|
|
48
|
-
if (!rendition) return currentState;
|
|
49
|
-
const newQueue = computeBufferQueue((await computeSegmentRangeAsync(seekTimeMs, seekTimeMs + config.bufferDurationMs, durationMs, rendition, deps.computeSegmentId)).filter((segmentId) => !deps.isSegmentCached(segmentId, rendition)), currentState.requestedSegments);
|
|
50
|
-
const newRequestedSegments = new Set(currentState.requestedSegments);
|
|
51
|
-
const newActiveRequests = new Set(currentState.activeRequests);
|
|
52
|
-
const remainingQueue = [...newQueue];
|
|
53
|
-
const startNextSegment = () => {
|
|
54
|
-
if (newActiveRequests.size >= config.maxParallelFetches || remainingQueue.length === 0 || signal.aborted) return;
|
|
55
|
-
const nextSegmentId = remainingQueue.shift();
|
|
56
|
-
if (nextSegmentId === void 0) return;
|
|
57
|
-
if (newRequestedSegments.has(nextSegmentId) || deps.isSegmentCached(nextSegmentId, rendition)) {
|
|
58
|
-
startNextSegment();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
newRequestedSegments.add(nextSegmentId);
|
|
62
|
-
newActiveRequests.add(nextSegmentId);
|
|
63
|
-
deps.prefetchSegment(nextSegmentId, rendition).then(() => {
|
|
64
|
-
if (signal.aborted) return;
|
|
65
|
-
newActiveRequests.delete(nextSegmentId);
|
|
66
|
-
if (config.enableContinuousBuffering ?? true) startNextSegment();
|
|
67
|
-
}).catch((error) => {
|
|
68
|
-
if (signal.aborted) return;
|
|
69
|
-
newActiveRequests.delete(nextSegmentId);
|
|
70
|
-
if (!(error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message.includes("signal is aborted") || error.message.includes("The user aborted a request")))) deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);
|
|
71
|
-
if (config.enableContinuousBuffering ?? true) startNextSegment();
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);
|
|
75
|
-
for (let i = 0; i < initialBatchSize; i++) startNextSegment();
|
|
76
|
-
return {
|
|
77
|
-
currentSeekTimeMs: seekTimeMs,
|
|
78
|
-
requestedSegments: newRequestedSegments,
|
|
79
|
-
activeRequests: newActiveRequests,
|
|
80
|
-
requestQueue: remainingQueue
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
//#endregion
|
|
85
|
-
export { manageMediaBuffer };
|
|
86
|
-
//# sourceMappingURL=BufferUtils.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BufferUtils.js","names":["segments: number[]"],"sources":["../../../../src/elements/EFMedia/shared/BufferUtils.ts"],"sourcesContent":["import type {\n AudioRendition,\n VideoRendition,\n} from \"../../../transcoding/types\";\n\n/**\n * State interface for media buffering - orchestration only, no data storage\n */\nexport interface MediaBufferState {\n currentSeekTimeMs: number;\n requestedSegments: Set<number>; // Segments we've requested for buffering\n activeRequests: Set<number>; // Segments currently being fetched\n requestQueue: number[]; // Segments queued to be requested\n}\n\n/**\n * Configuration interface for media buffering - generic for both audio and video\n */\nexport interface MediaBufferConfig {\n bufferDurationMs: number;\n maxParallelFetches: number;\n enableBuffering: boolean;\n enableContinuousBuffering?: boolean;\n bufferThresholdMs?: number; // Timeline-aware buffering threshold (default: 30000ms)\n}\n\n/**\n * Dependencies interface for media buffering - integrates with BaseMediaEngine\n */\nexport interface MediaBufferDependencies<\n T extends AudioRendition | VideoRendition,\n> {\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>;\n prefetchSegment: (segmentId: number, rendition: T) => Promise<void>; // Just trigger prefetch, don't return data\n isSegmentCached: (segmentId: number, rendition: T) => boolean; // Check BaseMediaEngine cache\n getRendition: () => Promise<T | undefined>;\n logError: (message: string, error: any) => void;\n}\n\n/**\n * Compute segment range for a time window\n * Pure function - determines which segments are needed for a time range\n */\nexport const computeSegmentRange = <T extends AudioRendition | VideoRendition>(\n startTimeMs: number,\n endTimeMs: number,\n rendition: T,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): number[] => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(endTimeMs / segmentDurationMs);\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const segmentId = computeSegmentId(i * segmentDurationMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Async version of computeSegmentRange for when computeSegmentId is async\n */\nexport const computeSegmentRangeAsync = async <\n T extends AudioRendition | VideoRendition,\n>(\n startTimeMs: number,\n endTimeMs: number,\n durationMs: number,\n rendition: T,\n computeSegmentId: (\n timeMs: number,\n rendition: T,\n ) => Promise<number | undefined>,\n): Promise<number[]> => {\n const segments: number[] = [];\n const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;\n\n // Calculate segment indices that overlap with [startTimeMs, endTimeMs]\n const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);\n const endSegmentIndex = Math.floor(\n Math.min(endTimeMs, durationMs) / segmentDurationMs,\n );\n\n for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {\n const timeMs = i * segmentDurationMs;\n if (timeMs < durationMs) {\n const segmentId = await computeSegmentId(timeMs, rendition);\n if (segmentId !== undefined) {\n segments.push(segmentId);\n }\n }\n }\n\n return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates\n};\n\n/**\n * Compute buffer queue based on desired segments and what we've already requested\n * Pure function - determines what new segments should be prefetched\n */\nexport const computeBufferQueue = (\n desiredSegments: number[],\n requestedSegments: Set<number>,\n): number[] => {\n return desiredSegments.filter(\n (segmentId) => !requestedSegments.has(segmentId),\n );\n};\n\n/**\n * Handle seek time change and recompute buffer queue\n * Pure function - computes new queue when seek time changes\n */\nexport const handleSeekTimeChange = <T extends AudioRendition | VideoRendition>(\n newSeekTimeMs: number,\n bufferDurationMs: number,\n rendition: T,\n currentState: MediaBufferState,\n computeSegmentId: (timeMs: number, rendition: T) => number | undefined,\n): { newQueue: number[]; overlappingRequests: number[] } => {\n const endTimeMs = newSeekTimeMs + bufferDurationMs;\n const desiredSegments = computeSegmentRange(\n newSeekTimeMs,\n endTimeMs,\n rendition,\n computeSegmentId,\n );\n\n // Find segments that are already being requested\n const overlappingRequests = desiredSegments.filter((segmentId) =>\n currentState.requestedSegments.has(segmentId),\n );\n\n const newQueue = computeBufferQueue(\n desiredSegments,\n currentState.requestedSegments,\n );\n\n return { newQueue, overlappingRequests };\n};\n\n/**\n * Check if a segment has been requested for buffering\n * Pure function for checking buffer orchestration state\n */\nexport const isSegmentRequested = (\n segmentId: number,\n bufferState: MediaBufferState | undefined,\n): boolean => {\n return bufferState?.requestedSegments.has(segmentId) ?? false;\n};\n\n/**\n * Get requested segments from a list of segment IDs\n * Pure function that returns which segments have been requested for buffering\n */\nexport const getRequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): Set<number> => {\n if (!bufferState) {\n return new Set();\n }\n return new Set(\n segmentIds.filter((id) => bufferState.requestedSegments.has(id)),\n );\n};\n\n/**\n * Get unrequested segments from a list of segment IDs\n * Pure function that returns which segments haven't been requested yet\n */\nexport const getUnrequestedSegments = (\n segmentIds: number[],\n bufferState: MediaBufferState | undefined,\n): number[] => {\n if (!bufferState) {\n return segmentIds;\n }\n return segmentIds.filter((id) => !bufferState.requestedSegments.has(id));\n};\n\n/**\n * Calculate distance from element to playhead position\n * Returns 0 if element is currently active, otherwise returns distance in milliseconds\n */\nexport const calculatePlayheadDistance = (\n element: { startTimeMs: number; endTimeMs: number },\n playheadMs: number,\n): number => {\n // Element hasn't started yet\n if (playheadMs < element.startTimeMs) {\n return element.startTimeMs - playheadMs;\n }\n // Element already finished\n if (playheadMs > element.endTimeMs) {\n return playheadMs - element.endTimeMs;\n }\n // Element is currently active\n return 0;\n};\n\n/**\n * Core media buffering orchestration logic - prefetch only, no data storage\n * Integrates with BaseMediaEngine's existing caching and request deduplication\n */\nexport const manageMediaBuffer = async <\n T extends AudioRendition | VideoRendition,\n>(\n seekTimeMs: number,\n config: MediaBufferConfig,\n currentState: MediaBufferState,\n durationMs: number,\n signal: AbortSignal,\n deps: MediaBufferDependencies<T>,\n timelineContext?: {\n elementStartMs: number;\n elementEndMs: number;\n playheadMs: number;\n },\n): Promise<MediaBufferState> => {\n if (!config.enableBuffering) {\n return currentState;\n }\n\n // Timeline-aware buffering: skip if element is too far from playhead\n if (timelineContext && config.bufferThresholdMs !== undefined) {\n const distance = calculatePlayheadDistance(\n {\n startTimeMs: timelineContext.elementStartMs,\n endTimeMs: timelineContext.elementEndMs,\n },\n timelineContext.playheadMs,\n );\n\n if (distance > config.bufferThresholdMs) {\n // Element is too far from playhead, skip buffering\n return currentState;\n }\n }\n\n const rendition = await deps.getRendition();\n if (!rendition) {\n // Cannot buffer without a rendition\n return currentState;\n }\n const endTimeMs = seekTimeMs + config.bufferDurationMs;\n\n const desiredSegments = await computeSegmentRangeAsync(\n seekTimeMs,\n endTimeMs,\n durationMs,\n rendition,\n deps.computeSegmentId,\n );\n // Filter out segments already cached by BaseMediaEngine\n const uncachedSegments = desiredSegments.filter(\n (segmentId) => !deps.isSegmentCached(segmentId, rendition),\n );\n\n const newQueue = computeBufferQueue(\n uncachedSegments,\n currentState.requestedSegments,\n );\n\n // Shared state for concurrency control - prevents race conditions\n const newRequestedSegments = new Set(currentState.requestedSegments);\n const newActiveRequests = new Set(currentState.activeRequests);\n const remainingQueue = [...newQueue];\n\n // Thread-safe function to start next segment when slot becomes available\n const startNextSegment = (): void => {\n // Check if we have capacity and segments to fetch\n if (\n newActiveRequests.size >= config.maxParallelFetches ||\n remainingQueue.length === 0 ||\n signal.aborted\n ) {\n return;\n }\n\n const nextSegmentId = remainingQueue.shift();\n if (nextSegmentId === undefined) return;\n\n // Skip if already requested or now cached\n if (\n newRequestedSegments.has(nextSegmentId) ||\n deps.isSegmentCached(nextSegmentId, rendition)\n ) {\n startNextSegment(); // Try next segment immediately\n return;\n }\n\n newRequestedSegments.add(nextSegmentId);\n newActiveRequests.add(nextSegmentId);\n\n // Start the prefetch request\n deps\n .prefetchSegment(nextSegmentId, rendition)\n .then(() => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n // Start next segment if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n })\n .catch((error) => {\n if (signal.aborted) return;\n newActiveRequests.delete(nextSegmentId);\n \n // Don't log AbortError - these are intentional request cancellations\n const isAbortError = \n error instanceof DOMException && error.name === \"AbortError\" ||\n (error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")\n ));\n \n if (!isAbortError) {\n deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);\n }\n \n // Continue even after error if continuous buffering is enabled\n if (config.enableContinuousBuffering ?? true) {\n startNextSegment();\n }\n });\n };\n\n // Start initial batch of requests up to maxParallelFetches limit\n const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);\n for (let i = 0; i < initialBatchSize; i++) {\n startNextSegment();\n }\n\n const result = {\n currentSeekTimeMs: seekTimeMs,\n requestedSegments: newRequestedSegments,\n activeRequests: newActiveRequests,\n requestQueue: remainingQueue, // What's left in the queue\n };\n return result;\n};\n"],"mappings":";;;;AAwEA,MAAa,2BAA2B,OAGtC,aACA,WACA,YACA,WACA,qBAIsB;CACtB,MAAMA,WAAqB,EAAE;CAC7B,MAAM,oBAAqB,UAAkB,qBAAqB;CAGlE,MAAM,oBAAoB,KAAK,MAAM,cAAc,kBAAkB;CACrE,MAAM,kBAAkB,KAAK,MAC3B,KAAK,IAAI,WAAW,WAAW,GAAG,kBACnC;AAED,MAAK,IAAI,IAAI,mBAAmB,KAAK,iBAAiB,KAAK;EACzD,MAAM,SAAS,IAAI;AACnB,MAAI,SAAS,YAAY;GACvB,MAAM,YAAY,MAAM,iBAAiB,QAAQ,UAAU;AAC3D,OAAI,cAAc,OAChB,UAAS,KAAK,UAAU;;;AAK9B,QAAO,SAAS,QAAQ,IAAI,OAAO,QAAQ,IAAI,QAAQ,GAAG,KAAK,MAAM;;;;;;AAOvE,MAAa,sBACX,iBACA,sBACa;AACb,QAAO,gBAAgB,QACpB,cAAc,CAAC,kBAAkB,IAAI,UAAU,CACjD;;;;;;AAgFH,MAAa,6BACX,SACA,eACW;AAEX,KAAI,aAAa,QAAQ,YACvB,QAAO,QAAQ,cAAc;AAG/B,KAAI,aAAa,QAAQ,UACvB,QAAO,aAAa,QAAQ;AAG9B,QAAO;;;;;;AAOT,MAAa,oBAAoB,OAG/B,YACA,QACA,cACA,YACA,QACA,MACA,oBAK8B;AAC9B,KAAI,CAAC,OAAO,gBACV,QAAO;AAIT,KAAI,mBAAmB,OAAO,sBAAsB,QASlD;MARiB,0BACf;GACE,aAAa,gBAAgB;GAC7B,WAAW,gBAAgB;GAC5B,EACD,gBAAgB,WACjB,GAEc,OAAO,kBAEpB,QAAO;;CAIX,MAAM,YAAY,MAAM,KAAK,cAAc;AAC3C,KAAI,CAAC,UAEH,QAAO;CAgBT,MAAM,WAAW,oBAZO,MAAM,yBAC5B,YAHgB,aAAa,OAAO,kBAKpC,YACA,WACA,KAAK,iBACN,EAEwC,QACtC,cAAc,CAAC,KAAK,gBAAgB,WAAW,UAAU,CAC3D,EAIC,aAAa,kBACd;CAGD,MAAM,uBAAuB,IAAI,IAAI,aAAa,kBAAkB;CACpE,MAAM,oBAAoB,IAAI,IAAI,aAAa,eAAe;CAC9D,MAAM,iBAAiB,CAAC,GAAG,SAAS;CAGpC,MAAM,yBAA+B;AAEnC,MACE,kBAAkB,QAAQ,OAAO,sBACjC,eAAe,WAAW,KAC1B,OAAO,QAEP;EAGF,MAAM,gBAAgB,eAAe,OAAO;AAC5C,MAAI,kBAAkB,OAAW;AAGjC,MACE,qBAAqB,IAAI,cAAc,IACvC,KAAK,gBAAgB,eAAe,UAAU,EAC9C;AACA,qBAAkB;AAClB;;AAGF,uBAAqB,IAAI,cAAc;AACvC,oBAAkB,IAAI,cAAc;AAGpC,OACG,gBAAgB,eAAe,UAAU,CACzC,WAAW;AACV,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AAEvC,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB,CACD,OAAO,UAAU;AAChB,OAAI,OAAO,QAAS;AACpB,qBAAkB,OAAO,cAAc;AAWvC,OAAI,EAPF,iBAAiB,gBAAgB,MAAM,SAAS,gBAC/C,iBAAiB,UAChB,MAAM,SAAS,gBACf,MAAM,QAAQ,SAAS,oBAAoB,IAC3C,MAAM,QAAQ,SAAS,6BAA6B,GAItD,MAAK,SAAS,8BAA8B,iBAAiB,MAAM;AAIrE,OAAI,OAAO,6BAA6B,KACtC,mBAAkB;IAEpB;;CAIN,MAAM,mBAAmB,KAAK,IAAI,OAAO,oBAAoB,SAAS,OAAO;AAC7E,MAAK,IAAI,IAAI,GAAG,IAAI,kBAAkB,IACpC,mBAAkB;AASpB,QANe;EACb,mBAAmB;EACnB,mBAAmB;EACnB,gBAAgB;EAChB,cAAc;EACf"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
|
|
2
|
-
import { AudioRendition, VideoRendition } from "../../../transcoding/types/index.js";
|
|
3
|
-
import { Task } from "@lit/task";
|
|
4
|
-
|
|
5
|
-
//#region src/elements/EFMedia/shared/MediaTaskUtils.d.ts
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Generic rendition type that can be either audio or video
|
|
9
|
-
*/
|
|
10
|
-
type MediaRendition = AudioRendition | VideoRendition;
|
|
11
|
-
/**
|
|
12
|
-
* Generic task type for input creation
|
|
13
|
-
*/
|
|
14
|
-
type InputTask = Task<readonly [ArrayBuffer, ArrayBuffer], BufferedSeekingInput | undefined>;
|
|
15
|
-
//#endregion
|
|
16
|
-
export { InputTask, MediaRendition };
|
|
17
|
-
//# sourceMappingURL=MediaTaskUtils.d.ts.map
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
2
|
-
import { AssetMediaEngine } from "../AssetMediaEngine.js";
|
|
3
|
-
import { AssetIdMediaEngine } from "../AssetIdMediaEngine.js";
|
|
4
|
-
import { JitMediaEngine } from "../JitMediaEngine.js";
|
|
5
|
-
import { Task } from "@lit/task";
|
|
6
|
-
|
|
7
|
-
//#region src/elements/EFMedia/tasks/makeMediaEngineTask.ts
|
|
8
|
-
const getLatestMediaEngine = async (host, signal) => {
|
|
9
|
-
let mediaEngine;
|
|
10
|
-
try {
|
|
11
|
-
mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
14
|
-
if (error instanceof Error && error.message === "No valid media source") return;
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
signal.throwIfAborted();
|
|
18
|
-
return mediaEngine || void 0;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Core logic for creating a MediaEngine with explicit dependencies.
|
|
22
|
-
* Pure function that requires all dependencies to be provided.
|
|
23
|
-
*
|
|
24
|
-
* @param host - The EFMedia element host
|
|
25
|
-
* @param signal - AbortSignal to cancel in-flight requests when element is disconnected
|
|
26
|
-
*/
|
|
27
|
-
const createMediaEngine = (host, signal) => {
|
|
28
|
-
const { src, assetId, urlGenerator, apiHost, requiredTracks } = host;
|
|
29
|
-
if (assetId !== null && assetId !== void 0 && assetId.trim() !== "") {
|
|
30
|
-
if (!apiHost) return Promise.reject(/* @__PURE__ */ new Error("API host is required for AssetID mode"));
|
|
31
|
-
return AssetIdMediaEngine.fetchByAssetId(host, urlGenerator, assetId, apiHost, requiredTracks, signal);
|
|
32
|
-
}
|
|
33
|
-
if (!src || typeof src !== "string" || src.trim() === "") {
|
|
34
|
-
console.error(`Unsupported media source: ${src}, assetId: ${assetId}`);
|
|
35
|
-
return Promise.reject(/* @__PURE__ */ new Error("Unsupported media source"));
|
|
36
|
-
}
|
|
37
|
-
const lowerSrc = src.toLowerCase();
|
|
38
|
-
const isRemoteUrl = lowerSrc.startsWith("http://") || lowerSrc.startsWith("https://");
|
|
39
|
-
const configuration = host.closest("ef-configuration");
|
|
40
|
-
if (configuration?.mediaEngine === "jit") {
|
|
41
|
-
let manifestSrc = src;
|
|
42
|
-
if (!isRemoteUrl && configuration.apiHost) manifestSrc = `${configuration.apiHost.replace(/\/$/, "")}${src.replace(/^\.\//, "/src/")}`;
|
|
43
|
-
const url$1 = urlGenerator.generateManifestUrl(manifestSrc);
|
|
44
|
-
return JitMediaEngine.fetch(host, urlGenerator, url$1, signal);
|
|
45
|
-
}
|
|
46
|
-
if (configuration?.mediaEngine === "local") return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);
|
|
47
|
-
if (!isRemoteUrl) return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);
|
|
48
|
-
const url = urlGenerator.generateManifestUrl(src);
|
|
49
|
-
return JitMediaEngine.fetch(host, urlGenerator, url, signal);
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Handle completion of media engine task - triggers necessary updates.
|
|
53
|
-
* Extracted for testability.
|
|
54
|
-
*/
|
|
55
|
-
const handleMediaEngineComplete = (host) => {
|
|
56
|
-
host.requestUpdate("intrinsicDurationMs");
|
|
57
|
-
host.requestUpdate("ownCurrentTimeMs");
|
|
58
|
-
if (host.rootTimegroup) queueMicrotask(() => {
|
|
59
|
-
host.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
|
|
60
|
-
host.rootTimegroup?.requestUpdate("durationMs");
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
const makeMediaEngineTask = (host) => {
|
|
64
|
-
let task;
|
|
65
|
-
task = new Task(host, {
|
|
66
|
-
autoRun: EF_INTERACTIVE,
|
|
67
|
-
args: () => [host.src, host.assetId],
|
|
68
|
-
task: async ([_src, _assetId], { signal }) => {
|
|
69
|
-
signal?.throwIfAborted();
|
|
70
|
-
const { src, assetId } = host;
|
|
71
|
-
if (assetId !== null && assetId !== void 0 && assetId.trim() !== "") return createMediaEngine(host, signal);
|
|
72
|
-
if (!src || typeof src !== "string" || src.trim() === "") return;
|
|
73
|
-
signal?.throwIfAborted();
|
|
74
|
-
return createMediaEngine(host, signal);
|
|
75
|
-
},
|
|
76
|
-
onError: (error) => {
|
|
77
|
-
task.taskComplete.catch(() => {});
|
|
78
|
-
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message.includes("signal is aborted") || error.message.includes("The user aborted a request")) || error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("404") || error.message.includes("Failed to fetch"))) return;
|
|
79
|
-
console.error("mediaEngineTask error", error);
|
|
80
|
-
},
|
|
81
|
-
onComplete: (value) => {
|
|
82
|
-
if (value) handleMediaEngineComplete(host);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
return task;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
//#endregion
|
|
89
|
-
export { getLatestMediaEngine, makeMediaEngineTask };
|
|
90
|
-
//# sourceMappingURL=makeMediaEngineTask.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeMediaEngineTask.js","names":["url","task: MediaEngineTask"],"sources":["../../../../src/elements/EFMedia/tasks/makeMediaEngineTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport { EF_INTERACTIVE } from \"../../../EF_INTERACTIVE\";\nimport type { MediaEngine } from \"../../../transcoding/types\";\nimport type { EFMedia } from \"../../EFMedia\";\nimport { AssetIdMediaEngine } from \"../AssetIdMediaEngine\";\nimport { AssetMediaEngine } from \"../AssetMediaEngine\";\nimport { JitMediaEngine } from \"../JitMediaEngine\";\n\nexport const getLatestMediaEngine = async (\n host: EFMedia,\n signal: AbortSignal,\n): Promise<MediaEngine | undefined> => {\n let mediaEngine;\n try {\n mediaEngine = await host.mediaEngineTask.taskComplete;\n } catch (error) {\n // If aborted, re-throw to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // If the error is \"No valid media source\", return undefined instead of throwing\n // This allows callers to handle missing media gracefully\n if (error instanceof Error && error.message === \"No valid media source\") {\n return undefined;\n }\n // For other errors, re-throw\n throw error;\n }\n signal.throwIfAborted();\n // Return undefined if no media engine (no valid source)\n // Callers should check for undefined and exit gracefully\n return mediaEngine || undefined;\n};\n\n/**\n * Core logic for creating a MediaEngine with explicit dependencies.\n * Pure function that requires all dependencies to be provided.\n * \n * @param host - The EFMedia element host\n * @param signal - AbortSignal to cancel in-flight requests when element is disconnected\n */\nexport const createMediaEngine = (host: EFMedia, signal?: AbortSignal): Promise<MediaEngine> => {\n const { src, assetId, urlGenerator, apiHost, requiredTracks } = host;\n\n // Check for AssetID mode first\n if (assetId !== null && assetId !== undefined && assetId.trim() !== \"\") {\n if (!apiHost) {\n return Promise.reject(new Error(\"API host is required for AssetID mode\"));\n }\n return AssetIdMediaEngine.fetchByAssetId(\n host,\n urlGenerator,\n assetId,\n apiHost,\n requiredTracks,\n signal,\n );\n }\n\n // Check for null/undefined/empty/whitespace src\n if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n console.error(`Unsupported media source: ${src}, assetId: ${assetId}`);\n return Promise.reject(new Error(\"Unsupported media source\"));\n }\n\n const lowerSrc = src.toLowerCase();\n const isRemoteUrl = lowerSrc.startsWith(\"http://\") || lowerSrc.startsWith(\"https://\");\n \n // Check configuration for explicit engine preference\n const configuration = host.closest(\"ef-configuration\");\n \n // \"jit\" mode: Force JitMediaEngine for all sources (including local files)\n if (configuration?.mediaEngine === \"jit\") {\n // For local paths, convert to full URL using apiHost\n let manifestSrc = src;\n if (!isRemoteUrl && configuration.apiHost) {\n // Convert relative path to absolute URL for the JIT manifest\n // e.g., \"./assets/video.mp4\" -> \"http://main.localhost:4321/src/assets/video.mp4\"\n const baseUrl = configuration.apiHost.replace(/\\/$/, \"\");\n const normalizedPath = src.replace(/^\\.\\//, \"/src/\");\n manifestSrc = `${baseUrl}${normalizedPath}`;\n }\n const url = urlGenerator.generateManifestUrl(manifestSrc);\n return JitMediaEngine.fetch(host, urlGenerator, url, signal);\n }\n \n // \"local\" mode: Force AssetMediaEngine for all sources\n if (configuration?.mediaEngine === \"local\") {\n return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);\n }\n \n // \"cloud\" mode (default): AssetMediaEngine for local paths, JitMediaEngine for remote URLs\n if (!isRemoteUrl) {\n return AssetMediaEngine.fetch(host, urlGenerator, src, requiredTracks, signal);\n }\n\n // Default: Use JitMediaEngine for remote URLs (transcoding service)\n const url = urlGenerator.generateManifestUrl(src);\n return JitMediaEngine.fetch(host, urlGenerator, url, signal);\n};\n\n/**\n * Handle completion of media engine task - triggers necessary updates.\n * Extracted for testability.\n */\nexport const handleMediaEngineComplete = (host: EFMedia): void => {\n // Update self synchronously - this is fine because we're updating the element\n // that just completed its task, not a parent\n host.requestUpdate(\"intrinsicDurationMs\");\n host.requestUpdate(\"ownCurrentTimeMs\");\n \n // Defer updates to parent/root timegroup to avoid Lit warning about scheduling\n // updates after update completed (change-in-update). Task onComplete can be\n // called during an update cycle, and directly calling requestUpdate on parent\n // elements causes the warning.\n if (host.rootTimegroup) {\n queueMicrotask(() => {\n host.rootTimegroup?.requestUpdate(\"ownCurrentTimeMs\");\n host.rootTimegroup?.requestUpdate(\"durationMs\");\n });\n }\n};\n\ntype MediaEngineTask = Task<readonly [string, string | null], MediaEngine>;\n\nexport const makeMediaEngineTask = (host: EFMedia): MediaEngineTask => {\n // Capture task reference for use in onError\n let task: MediaEngineTask;\n \n task = new Task(host, {\n autoRun: EF_INTERACTIVE,\n args: () => [host.src, host.assetId] as const,\n task: async ([_src, _assetId], { signal }) => {\n // Check abort before starting work\n signal?.throwIfAborted();\n \n // Check if we have a valid source before attempting to create media engine\n // This avoids unnecessary errors when src is empty/null/undefined\n const { src, assetId } = host;\n \n // If we have a valid assetId, proceed\n if (assetId !== null && assetId !== undefined && assetId.trim() !== \"\") {\n return createMediaEngine(host, signal);\n }\n \n // If we don't have a valid src, return undefined instead of throwing\n // This allows dependent tasks to check for undefined and exit gracefully\n // without logging errors for expected conditions\n if (!src || typeof src !== \"string\" || src.trim() === \"\") {\n return undefined as unknown as MediaEngine;\n }\n \n // Check abort before expensive operation\n signal?.throwIfAborted();\n \n return createMediaEngine(host, signal);\n },\n onError: (error) => {\n // CRITICAL: Attach .catch() handler to taskComplete BEFORE the promise is rejected.\n // onError is called synchronously before completeDeferred.reject() in Lit Task,\n // so this handler will be attached in time to prevent unhandled rejection.\n // Without this, the rejection from hostUpdate() -> _performTask() (which isn't awaited)\n // becomes an unhandled promise rejection.\n task.taskComplete.catch(() => {});\n \n // Don't log AbortError - these are intentional cancellations when element is disconnected\n const isAbortError = \n error instanceof DOMException && error.name === \"AbortError\" ||\n error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message.includes(\"signal is aborted\") ||\n error.message.includes(\"The user aborted a request\")\n );\n \n // Don't log errors when there's no valid media source or file not found - these are expected\n if (isAbortError || (error instanceof Error && (\n error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"404\") ||\n error.message.includes(\"Failed to fetch\")\n ))) {\n return;\n }\n \n // Log other unexpected errors\n console.error(\"mediaEngineTask error\", error);\n },\n onComplete: (value) => {\n // Only trigger updates if we actually got a media engine\n if (value) {\n handleMediaEngineComplete(host);\n }\n },\n });\n \n return task;\n};\n"],"mappings":";;;;;;;AAQA,MAAa,uBAAuB,OAClC,MACA,WACqC;CACrC,IAAI;AACJ,KAAI;AACF,gBAAc,MAAM,KAAK,gBAAgB;UAClC,OAAO;AAEd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAIR,MAAI,iBAAiB,SAAS,MAAM,YAAY,wBAC9C;AAGF,QAAM;;AAER,QAAO,gBAAgB;AAGvB,QAAO,eAAe;;;;;;;;;AAUxB,MAAa,qBAAqB,MAAe,WAA+C;CAC9F,MAAM,EAAE,KAAK,SAAS,cAAc,SAAS,mBAAmB;AAGhE,KAAI,YAAY,QAAQ,YAAY,UAAa,QAAQ,MAAM,KAAK,IAAI;AACtE,MAAI,CAAC,QACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,wCAAwC,CAAC;AAE3E,SAAO,mBAAmB,eACxB,MACA,cACA,SACA,SACA,gBACA,OACD;;AAIH,KAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,IAAI,MAAM,KAAK,IAAI;AACxD,UAAQ,MAAM,6BAA6B,IAAI,aAAa,UAAU;AACtE,SAAO,QAAQ,uBAAO,IAAI,MAAM,2BAA2B,CAAC;;CAG9D,MAAM,WAAW,IAAI,aAAa;CAClC,MAAM,cAAc,SAAS,WAAW,UAAU,IAAI,SAAS,WAAW,WAAW;CAGrF,MAAM,gBAAgB,KAAK,QAAQ,mBAAmB;AAGtD,KAAI,eAAe,gBAAgB,OAAO;EAExC,IAAI,cAAc;AAClB,MAAI,CAAC,eAAe,cAAc,QAKhC,eAAc,GAFE,cAAc,QAAQ,QAAQ,OAAO,GAAG,GACjC,IAAI,QAAQ,SAAS,QAAQ;EAGtD,MAAMA,QAAM,aAAa,oBAAoB,YAAY;AACzD,SAAO,eAAe,MAAM,MAAM,cAAcA,OAAK,OAAO;;AAI9D,KAAI,eAAe,gBAAgB,QACjC,QAAO,iBAAiB,MAAM,MAAM,cAAc,KAAK,gBAAgB,OAAO;AAIhF,KAAI,CAAC,YACH,QAAO,iBAAiB,MAAM,MAAM,cAAc,KAAK,gBAAgB,OAAO;CAIhF,MAAM,MAAM,aAAa,oBAAoB,IAAI;AACjD,QAAO,eAAe,MAAM,MAAM,cAAc,KAAK,OAAO;;;;;;AAO9D,MAAa,6BAA6B,SAAwB;AAGhE,MAAK,cAAc,sBAAsB;AACzC,MAAK,cAAc,mBAAmB;AAMtC,KAAI,KAAK,cACP,sBAAqB;AACnB,OAAK,eAAe,cAAc,mBAAmB;AACrD,OAAK,eAAe,cAAc,aAAa;GAC/C;;AAMN,MAAa,uBAAuB,SAAmC;CAErE,IAAIC;AAEJ,QAAO,IAAI,KAAK,MAAM;EACpB,SAAS;EACT,YAAY,CAAC,KAAK,KAAK,KAAK,QAAQ;EACpC,MAAM,OAAO,CAAC,MAAM,WAAW,EAAE,aAAa;AAE5C,WAAQ,gBAAgB;GAIxB,MAAM,EAAE,KAAK,YAAY;AAGzB,OAAI,YAAY,QAAQ,YAAY,UAAa,QAAQ,MAAM,KAAK,GAClE,QAAO,kBAAkB,MAAM,OAAO;AAMxC,OAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,IAAI,MAAM,KAAK,GACpD;AAIF,WAAQ,gBAAgB;AAExB,UAAO,kBAAkB,MAAM,OAAO;;EAExC,UAAU,UAAU;AAMlB,QAAK,aAAa,YAAY,GAAG;AAYjC,OARE,iBAAiB,gBAAgB,MAAM,SAAS,gBAChD,iBAAiB,UACf,MAAM,SAAS,gBACf,MAAM,QAAQ,SAAS,oBAAoB,IAC3C,MAAM,QAAQ,SAAS,6BAA6B,KAInC,iBAAiB,UACpC,MAAM,YAAY,2BAClB,MAAM,QAAQ,SAAS,iBAAiB,IACxC,MAAM,QAAQ,SAAS,MAAM,IAC7B,MAAM,QAAQ,SAAS,kBAAkB,EAEzC;AAIF,WAAQ,MAAM,yBAAyB,MAAM;;EAE/C,aAAa,UAAU;AAErB,OAAI,MACF,2BAA0B,KAAK;;EAGpC,CAAC;AAEF,QAAO"}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
2
|
-
import { EF_RENDERING } from "../../../EF_RENDERING.js";
|
|
3
|
-
import { manageMediaBuffer } from "../shared/BufferUtils.js";
|
|
4
|
-
import { Task } from "@lit/task";
|
|
5
|
-
|
|
6
|
-
//#region src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts
|
|
7
|
-
/**
|
|
8
|
-
* Scrub video buffer task - aggressively preloads the ENTIRE scrub track
|
|
9
|
-
* Unlike main video buffering, this loads the full duration with higher concurrency
|
|
10
|
-
* for instant visual feedback during seeking
|
|
11
|
-
*/
|
|
12
|
-
const makeScrubVideoBufferTask = (host) => {
|
|
13
|
-
let currentState = {
|
|
14
|
-
currentSeekTimeMs: 0,
|
|
15
|
-
requestedSegments: /* @__PURE__ */ new Set(),
|
|
16
|
-
activeRequests: /* @__PURE__ */ new Set(),
|
|
17
|
-
requestQueue: []
|
|
18
|
-
};
|
|
19
|
-
let task;
|
|
20
|
-
task = new Task(host, {
|
|
21
|
-
autoRun: EF_INTERACTIVE,
|
|
22
|
-
args: () => [host.mediaEngineTask.value],
|
|
23
|
-
onError: (error) => {
|
|
24
|
-
task.taskComplete.catch(() => {});
|
|
25
|
-
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
|
|
26
|
-
if (error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("is not valid JSON"))) return;
|
|
27
|
-
console.error("scrubVideoBufferTask error", error);
|
|
28
|
-
},
|
|
29
|
-
onComplete: (value) => {
|
|
30
|
-
currentState = value;
|
|
31
|
-
},
|
|
32
|
-
task: async ([mediaEngine], { signal }) => {
|
|
33
|
-
if (EF_RENDERING()) return currentState;
|
|
34
|
-
if (!host.enableVideoBuffering) return currentState;
|
|
35
|
-
if (!mediaEngine) return currentState;
|
|
36
|
-
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
37
|
-
if (!scrubRendition) return currentState;
|
|
38
|
-
const scrubRenditionWithSrc = {
|
|
39
|
-
...scrubRendition,
|
|
40
|
-
src: mediaEngine.src
|
|
41
|
-
};
|
|
42
|
-
try {
|
|
43
|
-
try {
|
|
44
|
-
await mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.warn("ScrubBuffer: Failed to cache scrub init segment:", error);
|
|
47
|
-
}
|
|
48
|
-
return await manageMediaBuffer(0, {
|
|
49
|
-
bufferDurationMs: mediaEngine.durationMs,
|
|
50
|
-
maxParallelFetches: 10,
|
|
51
|
-
enableBuffering: true,
|
|
52
|
-
enableContinuousBuffering: true
|
|
53
|
-
}, currentState, mediaEngine.durationMs, signal, {
|
|
54
|
-
computeSegmentId: async (timeMs, rendition) => {
|
|
55
|
-
return mediaEngine.computeSegmentId(timeMs, rendition);
|
|
56
|
-
},
|
|
57
|
-
prefetchSegment: async (segmentId, rendition) => {
|
|
58
|
-
await mediaEngine.fetchMediaSegment(segmentId, rendition, signal);
|
|
59
|
-
},
|
|
60
|
-
isSegmentCached: (segmentId, rendition) => {
|
|
61
|
-
return mediaEngine.isSegmentCached(segmentId, rendition);
|
|
62
|
-
},
|
|
63
|
-
getRendition: async () => scrubRenditionWithSrc,
|
|
64
|
-
logError: (message, error) => {
|
|
65
|
-
console.warn(`ScrubBuffer: ${message}`, error);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
} catch (error) {
|
|
69
|
-
if (signal?.aborted) return currentState;
|
|
70
|
-
console.warn("ScrubBuffer failed:", error);
|
|
71
|
-
return currentState;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
return task;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
//#endregion
|
|
79
|
-
export { makeScrubVideoBufferTask };
|
|
80
|
-
//# sourceMappingURL=makeScrubVideoBufferTask.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeScrubVideoBufferTask.js","names":["currentState: MediaBufferState","task: Task<readonly [any], MediaBufferState>"],"sources":["../../../../src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\n\nimport { EF_INTERACTIVE } from \"../../../EF_INTERACTIVE.js\";\nimport { EF_RENDERING } from \"../../../EF_RENDERING.js\";\nimport type { VideoRendition } from \"../../../transcoding/types\";\nimport type { EFVideo } from \"../../EFVideo\";\nimport type { MediaBufferState } from \"../shared/BufferUtils\";\nimport { manageMediaBuffer } from \"../shared/BufferUtils\";\n\n/**\n * Scrub video buffer task - aggressively preloads the ENTIRE scrub track\n * Unlike main video buffering, this loads the full duration with higher concurrency\n * for instant visual feedback during seeking\n */\nexport const makeScrubVideoBufferTask = (host: EFVideo): Task<readonly [any], MediaBufferState> => {\n let currentState: MediaBufferState = {\n currentSeekTimeMs: 0,\n requestedSegments: new Set(),\n activeRequests: new Set(),\n requestQueue: [],\n };\n\n // Capture task reference for use in onError\n let task: Task<readonly [any], MediaBufferState>;\n\n task = new Task(host, {\n autoRun: EF_INTERACTIVE,\n args: () => [host.mediaEngineTask.value] as const,\n onError: (error) => {\n // CRITICAL: Attach .catch() handler to taskComplete BEFORE the promise is rejected.\n // This prevents unhandled rejection when hostUpdate() triggers _performTask() without awaiting.\n task.taskComplete.catch(() => {});\n \n // Don't log AbortErrors - these are expected when tasks are cancelled\n if (\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message?.includes(\"signal is aborted\") ||\n error.message?.includes(\"The user aborted a request\")\n ))\n ) {\n return;\n }\n // Don't log errors when there's no valid media source or file not found - these are expected\n if (error instanceof Error && (\n error.message === \"No valid media source\" ||\n error.message.includes(\"File not found\") ||\n error.message.includes(\"is not valid JSON\")\n )) {\n return;\n }\n console.error(\"scrubVideoBufferTask error\", error);\n },\n onComplete: (value) => {\n currentState = value;\n },\n task: async ([mediaEngine], { signal }) => {\n // Skip entirely in rendering mode\n if (EF_RENDERING()) {\n return currentState;\n }\n\n // Don't run if video buffering is disabled\n if (!host.enableVideoBuffering) {\n return currentState;\n }\n\n // Need media engine to be available\n if (!mediaEngine) {\n return currentState;\n }\n\n // Get scrub rendition using the proper interface method\n const scrubRendition = mediaEngine.getScrubVideoRendition();\n\n if (!scrubRendition) {\n // No scrub rendition available - this is fine, just return current state\n return currentState;\n }\n\n // Add src to rendition\n const scrubRenditionWithSrc = {\n ...scrubRendition,\n src: mediaEngine.src,\n };\n\n try {\n // First, ensure the init segment is cached for scrub content\n try {\n await mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);\n } catch (error) {\n console.warn(\n \"ScrubBuffer: Failed to cache scrub init segment:\",\n error,\n );\n }\n\n // Aggressively buffer the ENTIRE scrub track\n const newState = await manageMediaBuffer<VideoRendition>(\n 0, // Always start from beginning to ensure complete coverage\n {\n // Buffer entire duration - scrub segments are 30s so much fewer total segments\n bufferDurationMs: mediaEngine.durationMs,\n // High concurrency for scrub - fewer segments overall (30s each vs 2s for main)\n // E.g., 10min video = ~20 scrub segments vs ~300 main segments\n maxParallelFetches: 10,\n enableBuffering: true,\n enableContinuousBuffering: true, // Keep going until all segments loaded\n },\n currentState,\n mediaEngine.durationMs,\n signal,\n {\n computeSegmentId: async (timeMs, rendition) => {\n return mediaEngine.computeSegmentId(timeMs, rendition);\n },\n prefetchSegment: async (segmentId, rendition) => {\n // Use the media engine to fetch and cache scrub segments\n await mediaEngine.fetchMediaSegment(segmentId, rendition, signal);\n },\n isSegmentCached: (segmentId, rendition) => {\n return mediaEngine.isSegmentCached(segmentId, rendition);\n },\n getRendition: async () => scrubRenditionWithSrc,\n logError: (message, error) => {\n console.warn(`ScrubBuffer: ${message}`, error);\n },\n },\n );\n\n return newState;\n } catch (error) {\n if (signal?.aborted) return currentState;\n console.warn(\"ScrubBuffer failed:\", error);\n return currentState;\n }\n },\n });\n\n return task;\n};\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,4BAA4B,SAA0D;CACjG,IAAIA,eAAiC;EACnC,mBAAmB;EACnB,mCAAmB,IAAI,KAAK;EAC5B,gCAAgB,IAAI,KAAK;EACzB,cAAc,EAAE;EACjB;CAGD,IAAIC;AAEJ,QAAO,IAAI,KAAK,MAAM;EACpB,SAAS;EACT,YAAY,CAAC,KAAK,gBAAgB,MAAM;EACxC,UAAU,UAAU;AAGlB,QAAK,aAAa,YAAY,GAAG;AAGjC,OACG,iBAAiB,gBAAgB,MAAM,SAAS,gBAChD,iBAAiB,UAChB,MAAM,SAAS,gBACf,MAAM,SAAS,SAAS,oBAAoB,IAC5C,MAAM,SAAS,SAAS,6BAA6B,EAGvD;AAGF,OAAI,iBAAiB,UACnB,MAAM,YAAY,2BAClB,MAAM,QAAQ,SAAS,iBAAiB,IACxC,MAAM,QAAQ,SAAS,oBAAoB,EAE3C;AAEF,WAAQ,MAAM,8BAA8B,MAAM;;EAEpD,aAAa,UAAU;AACrB,kBAAe;;EAEjB,MAAM,OAAO,CAAC,cAAc,EAAE,aAAa;AAEzC,OAAI,cAAc,CAChB,QAAO;AAIT,OAAI,CAAC,KAAK,qBACR,QAAO;AAIT,OAAI,CAAC,YACH,QAAO;GAIT,MAAM,iBAAiB,YAAY,wBAAwB;AAE3D,OAAI,CAAC,eAEH,QAAO;GAIT,MAAM,wBAAwB;IAC5B,GAAG;IACH,KAAK,YAAY;IAClB;AAED,OAAI;AAEF,QAAI;AACF,WAAM,YAAY,iBAAiB,uBAAuB,OAAO;aAC1D,OAAO;AACd,aAAQ,KACN,oDACA,MACD;;AAoCH,WAhCiB,MAAM,kBACrB,GACA;KAEE,kBAAkB,YAAY;KAG9B,oBAAoB;KACpB,iBAAiB;KACjB,2BAA2B;KAC5B,EACD,cACA,YAAY,YACZ,QACA;KACE,kBAAkB,OAAO,QAAQ,cAAc;AAC7C,aAAO,YAAY,iBAAiB,QAAQ,UAAU;;KAExD,iBAAiB,OAAO,WAAW,cAAc;AAE/C,YAAM,YAAY,kBAAkB,WAAW,WAAW,OAAO;;KAEnE,kBAAkB,WAAW,cAAc;AACzC,aAAO,YAAY,gBAAgB,WAAW,UAAU;;KAE1D,cAAc,YAAY;KAC1B,WAAW,SAAS,UAAU;AAC5B,cAAQ,KAAK,gBAAgB,WAAW,MAAM;;KAEjD,CACF;YAGM,OAAO;AACd,QAAI,QAAQ,QAAS,QAAO;AAC5B,YAAQ,KAAK,uBAAuB,MAAM;AAC1C,WAAO;;;EAGZ,CAAC;AAEF,QAAO"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { AssetMediaEngine } from "../AssetMediaEngine.js";
|
|
2
|
-
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
|
|
3
|
-
import { Task } from "@lit/task";
|
|
4
|
-
|
|
5
|
-
//#region src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts
|
|
6
|
-
const makeScrubVideoInitSegmentFetchTask = (host) => {
|
|
7
|
-
let task;
|
|
8
|
-
task = new Task(host, {
|
|
9
|
-
args: () => [host.mediaEngineTask.value],
|
|
10
|
-
onError: (error) => {
|
|
11
|
-
task.taskComplete.catch(() => {});
|
|
12
|
-
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
|
|
13
|
-
if (error instanceof Error && error.message !== "No scrub rendition available") console.error("scrubVideoInitSegmentFetchTask error", error);
|
|
14
|
-
},
|
|
15
|
-
onComplete: (_value) => {},
|
|
16
|
-
task: async ([mediaEngineValue], { signal }) => {
|
|
17
|
-
if (host.mediaEngineTask.error || !mediaEngineValue) return;
|
|
18
|
-
let mediaEngine;
|
|
19
|
-
try {
|
|
20
|
-
mediaEngine = await getLatestMediaEngine(host, signal);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (error instanceof Error && error.message === "No valid media source") return;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
if (!mediaEngine) return;
|
|
26
|
-
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
27
|
-
if (!scrubRendition) return;
|
|
28
|
-
if (mediaEngine instanceof AssetMediaEngine && scrubRendition.trackId !== -1) {
|
|
29
|
-
const trackData = mediaEngine.data?.[scrubRendition.trackId];
|
|
30
|
-
if (!trackData || !trackData.initSegment) return;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
return await mediaEngine.fetchInitSegment({
|
|
34
|
-
...scrubRendition,
|
|
35
|
-
src: mediaEngine.src
|
|
36
|
-
}, signal);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
39
|
-
if (error instanceof Error && (error.message.includes("Init segment not found") || error.message.includes("Track not found") || error.message.includes("Failed to fetch") || error.message.includes("File not found"))) return;
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return task;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
export { makeScrubVideoInitSegmentFetchTask };
|
|
49
|
-
//# sourceMappingURL=makeScrubVideoInitSegmentFetchTask.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeScrubVideoInitSegmentFetchTask.js","names":["task: ScrubVideoInitSegmentFetchTask"],"sources":["../../../../src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport type { MediaEngine } from \"../../../transcoding/types\";\nimport type { EFVideo } from \"../../EFVideo\";\nimport { AssetMediaEngine } from \"../AssetMediaEngine\";\nimport { getLatestMediaEngine } from \"../tasks/makeMediaEngineTask\";\n\ntype ScrubVideoInitSegmentFetchTask = Task<readonly [MediaEngine | undefined], ArrayBuffer>;\n\nexport const makeScrubVideoInitSegmentFetchTask = (\n host: EFVideo,\n): ScrubVideoInitSegmentFetchTask => {\n // Capture task reference for use in onError\n let task: ScrubVideoInitSegmentFetchTask;\n\n task = new Task(host, {\n args: () => [host.mediaEngineTask.value] as const,\n onError: (error) => {\n // CRITICAL: Attach .catch() handler to taskComplete BEFORE the promise is rejected.\n // This prevents unhandled rejection when hostUpdate() triggers _performTask() without awaiting.\n task.taskComplete.catch(() => {});\n \n // Don't log AbortErrors - these are expected when tasks are cancelled\n if (\n (error instanceof DOMException && error.name === \"AbortError\") ||\n (error instanceof Error && (\n error.name === \"AbortError\" ||\n error.message?.includes(\"signal is aborted\") ||\n error.message?.includes(\"The user aborted a request\")\n ))\n ) {\n return;\n }\n // Only log unexpected errors - missing scrub rendition is handled gracefully above\n if (error instanceof Error && error.message !== \"No scrub rendition available\") {\n console.error(\"scrubVideoInitSegmentFetchTask error\", error);\n }\n },\n onComplete: (_value) => {},\n task: async ([mediaEngineValue], { signal }) => {\n // Check if media engine task has errored (no valid source) before attempting to use it\n if (host.mediaEngineTask.error || !mediaEngineValue) {\n return undefined as any;\n }\n \n let mediaEngine;\n try {\n mediaEngine = await getLatestMediaEngine(host, signal);\n } catch (error) {\n // If media engine task failed (no valid source), return undefined silently\n if (error instanceof Error && error.message === \"No valid media source\") {\n return undefined as any;\n }\n // Re-throw unexpected errors\n throw error;\n }\n \n // Return undefined if no valid media engine (no valid source)\n if (!mediaEngine) {\n return undefined as any;\n }\n\n // Get scrub rendition using the proper interface method\n const scrubRendition = mediaEngine.getScrubVideoRendition();\n\n if (!scrubRendition) {\n // No scrub rendition available - this is fine, scrub is optional\n // Return undefined instead of throwing to avoid error noise\n return undefined as any; // Task expects ArrayBuffer, but undefined indicates unavailable\n }\n\n // Check if the track exists in AssetMediaEngine data before fetching init segment\n // Scrub track uses trackId -1, which is handled specially, so skip check for that\n if (mediaEngine instanceof AssetMediaEngine && scrubRendition.trackId !== -1) {\n // @ts-expect-error - data is protected but we need to check track existence\n const trackData = (mediaEngine as any).data?.[scrubRendition.trackId];\n if (!trackData || !trackData.initSegment) {\n // Track doesn't exist or has no init segment - don't fetch\n return undefined as any;\n }\n }\n\n // Try to fetch the init segment, but return undefined if it fails\n try {\n return await mediaEngine.fetchInitSegment(\n {\n ...scrubRendition,\n src: mediaEngine.src, // Ensure src is set\n },\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 init segment doesn't exist or fetch fails, return undefined gracefully\n if (\n error instanceof Error &&\n (error.message.includes(\"Init segment not found\") ||\n error.message.includes(\"Track not found\") ||\n error.message.includes(\"Failed to fetch\") ||\n error.message.includes(\"File not found\"))\n ) {\n return undefined as any;\n }\n // Re-throw unexpected errors\n throw error;\n }\n },\n });\n\n return task;\n};\n"],"mappings":";;;;;AAQA,MAAa,sCACX,SACmC;CAEnC,IAAIA;AAEJ,QAAO,IAAI,KAAK,MAAM;EACpB,YAAY,CAAC,KAAK,gBAAgB,MAAM;EACxC,UAAU,UAAU;AAGlB,QAAK,aAAa,YAAY,GAAG;AAGjC,OACG,iBAAiB,gBAAgB,MAAM,SAAS,gBAChD,iBAAiB,UAChB,MAAM,SAAS,gBACf,MAAM,SAAS,SAAS,oBAAoB,IAC5C,MAAM,SAAS,SAAS,6BAA6B,EAGvD;AAGF,OAAI,iBAAiB,SAAS,MAAM,YAAY,+BAC9C,SAAQ,MAAM,wCAAwC,MAAM;;EAGhE,aAAa,WAAW;EACxB,MAAM,OAAO,CAAC,mBAAmB,EAAE,aAAa;AAE9C,OAAI,KAAK,gBAAgB,SAAS,CAAC,iBACjC;GAGF,IAAI;AACJ,OAAI;AACF,kBAAc,MAAM,qBAAqB,MAAM,OAAO;YAC/C,OAAO;AAEd,QAAI,iBAAiB,SAAS,MAAM,YAAY,wBAC9C;AAGF,UAAM;;AAIR,OAAI,CAAC,YACH;GAIF,MAAM,iBAAiB,YAAY,wBAAwB;AAE3D,OAAI,CAAC,eAGH;AAKF,OAAI,uBAAuB,oBAAoB,eAAe,YAAY,IAAI;IAE5E,MAAM,YAAa,YAAoB,OAAO,eAAe;AAC7D,QAAI,CAAC,aAAa,CAAC,UAAU,YAE3B;;AAKJ,OAAI;AACF,WAAO,MAAM,YAAY,iBACvB;KACE,GAAG;KACH,KAAK,YAAY;KAClB,EACD,OACD;YACM,OAAO;AAEd,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAGR,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,yBAAyB,IAC/C,MAAM,QAAQ,SAAS,kBAAkB,IACzC,MAAM,QAAQ,SAAS,kBAAkB,IACzC,MAAM,QAAQ,SAAS,iBAAiB,EAE1C;AAGF,UAAM;;;EAGX,CAAC;AAEF,QAAO"}
|