@editframe/elements 0.33.0-beta → 0.34.5-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 +99 -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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region src/preview/FrameController.ts
|
|
2
|
+
/**
|
|
3
|
+
* Priority for video elements.
|
|
4
|
+
* Video renders first as other elements may depend on video frames being ready.
|
|
5
|
+
*/
|
|
6
|
+
const PRIORITY_VIDEO = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Priority for captions elements.
|
|
9
|
+
* Captions render after video so they can overlay correctly.
|
|
10
|
+
*/
|
|
11
|
+
const PRIORITY_CAPTIONS = 2;
|
|
12
|
+
/**
|
|
13
|
+
* Priority for audio elements.
|
|
14
|
+
* Audio renders after captions (no visual dependency, but keeps consistent ordering).
|
|
15
|
+
*/
|
|
16
|
+
const PRIORITY_AUDIO = 3;
|
|
17
|
+
/**
|
|
18
|
+
* Priority for waveform elements.
|
|
19
|
+
* Waveform renders after audio because it depends on audio analysis data.
|
|
20
|
+
*/
|
|
21
|
+
const PRIORITY_WAVEFORM = 4;
|
|
22
|
+
/**
|
|
23
|
+
* Priority for image elements.
|
|
24
|
+
* Images render with low priority as they're typically static.
|
|
25
|
+
*/
|
|
26
|
+
const PRIORITY_IMAGE = 5;
|
|
27
|
+
/**
|
|
28
|
+
* Type guard to check if an element implements FrameRenderable.
|
|
29
|
+
*/
|
|
30
|
+
function isFrameRenderable(element) {
|
|
31
|
+
return typeof element === "object" && element !== null && "getFrameState" in element && "prepareFrame" in element && "renderFrame" in element && typeof element.getFrameState === "function" && typeof element.prepareFrame === "function" && typeof element.renderFrame === "function";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Central controller for frame rendering.
|
|
35
|
+
* Lives at the root timegroup and orchestrates all element rendering.
|
|
36
|
+
*/
|
|
37
|
+
var FrameController = class {
|
|
38
|
+
#rootElement;
|
|
39
|
+
#abortController = null;
|
|
40
|
+
#renderInProgress = false;
|
|
41
|
+
#pendingRenderTime = null;
|
|
42
|
+
constructor(rootElement) {
|
|
43
|
+
this.#rootElement = rootElement;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Cancel any in-progress render operation.
|
|
47
|
+
*/
|
|
48
|
+
abort() {
|
|
49
|
+
this.#abortController?.abort();
|
|
50
|
+
this.#abortController = null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Render a frame at the specified time.
|
|
54
|
+
*
|
|
55
|
+
* This is the main entry point for frame rendering. It:
|
|
56
|
+
* 1. Cancels any previous in-progress render
|
|
57
|
+
* 2. Queries all visible FrameRenderable elements
|
|
58
|
+
* 3. Runs preparation in parallel for elements that need it
|
|
59
|
+
* 4. Runs render in priority order
|
|
60
|
+
*
|
|
61
|
+
* @param timeMs - The time in milliseconds to render
|
|
62
|
+
* @param options - Optional configuration
|
|
63
|
+
*/
|
|
64
|
+
async renderFrame(timeMs, options = {}) {
|
|
65
|
+
const { waitForLitUpdate = true, onAnimationsUpdate } = options;
|
|
66
|
+
if (this.#renderInProgress) {
|
|
67
|
+
this.#pendingRenderTime = timeMs;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.#abortController?.abort();
|
|
71
|
+
this.#abortController = new AbortController();
|
|
72
|
+
const signal = this.#abortController.signal;
|
|
73
|
+
this.#renderInProgress = true;
|
|
74
|
+
try {
|
|
75
|
+
if (waitForLitUpdate) {
|
|
76
|
+
await this.#rootElement.updateComplete;
|
|
77
|
+
signal.throwIfAborted();
|
|
78
|
+
}
|
|
79
|
+
const elements = this.#queryVisibleElements(timeMs);
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
const elementsNeedingPreparation = elements.filter((el) => el.getFrameState(timeMs).needsPreparation);
|
|
82
|
+
if (elementsNeedingPreparation.length > 0) {
|
|
83
|
+
await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
}
|
|
86
|
+
const sortedElements = [...elements].sort((a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority);
|
|
87
|
+
for (const element of sortedElements) {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
element.renderFrame(timeMs);
|
|
90
|
+
}
|
|
91
|
+
if (onAnimationsUpdate) onAnimationsUpdate(this.#rootElement);
|
|
92
|
+
} finally {
|
|
93
|
+
this.#renderInProgress = false;
|
|
94
|
+
if (this.#pendingRenderTime !== null) {
|
|
95
|
+
const pendingTime = this.#pendingRenderTime;
|
|
96
|
+
this.#pendingRenderTime = null;
|
|
97
|
+
this.renderFrame(pendingTime, options).catch(() => {});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Query all visible FrameRenderable elements in the tree.
|
|
103
|
+
* Uses temporal visibility to filter out elements not visible at current time.
|
|
104
|
+
*
|
|
105
|
+
* IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)
|
|
106
|
+
* instead of CSS visibility. This is because updateAnimations sets display:none on
|
|
107
|
+
* elements outside their time range, but that CSS state is from the PREVIOUS frame.
|
|
108
|
+
* When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.
|
|
109
|
+
*
|
|
110
|
+
* @param timeMs - The time to use for visibility checks. This should be the target
|
|
111
|
+
* render time, not read from root element (which may be stale).
|
|
112
|
+
*/
|
|
113
|
+
#queryVisibleElements(timeMs) {
|
|
114
|
+
const result = [];
|
|
115
|
+
const currentTimeMs = timeMs;
|
|
116
|
+
const walk = (element) => {
|
|
117
|
+
if ("startTimeMs" in element && "endTimeMs" in element) {
|
|
118
|
+
const startMs = element.startTimeMs ?? -Infinity;
|
|
119
|
+
const endMs = element.endTimeMs ?? Infinity;
|
|
120
|
+
if (!(currentTimeMs >= startMs && currentTimeMs < endMs)) return;
|
|
121
|
+
if (isFrameRenderable(element)) result.push(element);
|
|
122
|
+
} else {
|
|
123
|
+
if (element instanceof HTMLElement) {
|
|
124
|
+
if (element.style.display === "none") return;
|
|
125
|
+
const style = getComputedStyle(element);
|
|
126
|
+
if (style.display === "none" || style.visibility === "hidden") return;
|
|
127
|
+
}
|
|
128
|
+
if (isFrameRenderable(element)) result.push(element);
|
|
129
|
+
}
|
|
130
|
+
const children = this.#getChildrenIncludingSlotted(element);
|
|
131
|
+
for (const child of children) walk(child);
|
|
132
|
+
};
|
|
133
|
+
walk(this.#rootElement);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Gets all child elements including slotted content for shadow DOM elements.
|
|
138
|
+
* For elements with shadow DOM that contain slots, this returns the assigned
|
|
139
|
+
* elements (slotted content) instead of just the shadow DOM children.
|
|
140
|
+
*/
|
|
141
|
+
#getChildrenIncludingSlotted(element) {
|
|
142
|
+
if (element.shadowRoot) {
|
|
143
|
+
const slots = element.shadowRoot.querySelectorAll("slot");
|
|
144
|
+
if (slots.length > 0) {
|
|
145
|
+
const assignedElements = [];
|
|
146
|
+
for (const slot of slots) assignedElements.push(...slot.assignedElements());
|
|
147
|
+
for (const child of element.shadowRoot.children) if (child.tagName !== "SLOT") assignedElements.push(child);
|
|
148
|
+
return assignedElements;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return Array.from(element.children);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a render is currently in progress.
|
|
155
|
+
*/
|
|
156
|
+
get isRendering() {
|
|
157
|
+
return this.#renderInProgress;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Create a backwards-compatible frameTask wrapper for a FrameRenderable element.
|
|
162
|
+
*
|
|
163
|
+
* This factory function creates a frameTask object that:
|
|
164
|
+
* - Manages its own AbortController for cancellation
|
|
165
|
+
* - Calls prepareFrame() then renderFrame() in sequence
|
|
166
|
+
* - Silently ignores AbortErrors (expected during cancellation)
|
|
167
|
+
* - Provides taskComplete promise for awaiting completion
|
|
168
|
+
*
|
|
169
|
+
* @param element - The element implementing FrameRenderable
|
|
170
|
+
* @param options - Optional configuration
|
|
171
|
+
* @returns A frameTask object compatible with legacy code
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* class MyElement extends LitElement implements FrameRenderable {
|
|
176
|
+
* frameTask = createFrameTaskWrapper(this, {
|
|
177
|
+
* getTimeMs: () => this.currentSourceTimeMs,
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* getFrameState(timeMs: number): FrameState { ... }
|
|
181
|
+
* async prepareFrame(timeMs: number, signal: AbortSignal): Promise<void> { ... }
|
|
182
|
+
* renderFrame(timeMs: number): void { ... }
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function createFrameTaskWrapper(element, options = {}) {
|
|
187
|
+
let frameTaskPromise = Promise.resolve();
|
|
188
|
+
const getTimeMs = options.getTimeMs ?? (() => {
|
|
189
|
+
if ("desiredSeekTimeMs" in element && typeof element.desiredSeekTimeMs === "number") return element.desiredSeekTimeMs;
|
|
190
|
+
if ("ownCurrentTimeMs" in element && typeof element.ownCurrentTimeMs === "number") return element.ownCurrentTimeMs;
|
|
191
|
+
return 0;
|
|
192
|
+
});
|
|
193
|
+
const taskObj = {
|
|
194
|
+
run: () => {
|
|
195
|
+
const abortController = new AbortController();
|
|
196
|
+
const timeMs = getTimeMs();
|
|
197
|
+
frameTaskPromise = (async () => {
|
|
198
|
+
try {
|
|
199
|
+
await element.prepareFrame(timeMs, abortController.signal);
|
|
200
|
+
element.renderFrame(timeMs);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
taskObj.taskComplete = frameTaskPromise;
|
|
207
|
+
return frameTaskPromise;
|
|
208
|
+
},
|
|
209
|
+
taskComplete: Promise.resolve()
|
|
210
|
+
};
|
|
211
|
+
return taskObj;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
export { FrameController, PRIORITY_AUDIO, PRIORITY_CAPTIONS, PRIORITY_IMAGE, PRIORITY_VIDEO, PRIORITY_WAVEFORM, createFrameTaskWrapper };
|
|
216
|
+
//# sourceMappingURL=FrameController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]","frameTaskPromise: Promise<void>","taskObj: FrameTask"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n * \n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(element: unknown): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {}\n ): Promise<void> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n // Wait for Lit to propagate time changes to children\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n // Query all visible elements that implement FrameRenderable\n // Pass the timeMs parameter to use for visibility checks (root element's time may be stale)\n const elements = this.#queryVisibleElements(timeMs);\n signal.throwIfAborted();\n\n // Phase 1: Parallel preparation\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(\n elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal))\n );\n signal.throwIfAborted();\n }\n\n // Phase 2: Sequential render by priority\n const sortedElements = [...elements].sort(\n (a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n\n // Phase 3: Update CSS animations (centralized)\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n * \n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n * \n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n \n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs = (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible = currentTimeMs >= startMs && currentTimeMs < endMs;\n \n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n \n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: use CSS visibility\n if (element instanceof HTMLElement) {\n // Fast path: check inline display style\n if (element.style.display === \"none\") {\n return;\n }\n // Slow path: check computed style\n const style = getComputedStyle(element);\n if (style.display === \"none\" || style.visibility === \"hidden\") {\n return;\n }\n }\n\n // Check if this element implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Element[] {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll('slot');\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== 'SLOT') {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n \n // Fallback to regular children\n return Array.from(element.children);\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<T extends { new (...args: any[]): HTMLElement }>(\n Base: T\n) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n\n// ============================================================================\n// Shared Frame Task Wrapper\n// ============================================================================\n// Creates a backwards-compatible frameTask object from a FrameRenderable element.\n// This eliminates duplicate boilerplate code across all temporal elements.\n// ============================================================================\n\n/**\n * Interface for the legacy frameTask object.\n * Used for backwards compatibility with code expecting the old Task-like API.\n */\nexport interface FrameTask {\n /**\n * Run the frame task (prepare + render).\n * @returns Promise that resolves when the task completes\n */\n run(): Promise<void>;\n\n /**\n * Promise that resolves when the current task completes.\n */\n taskComplete: Promise<void>;\n}\n\n/**\n * Options for creating a frame task wrapper.\n */\nexport interface FrameTaskWrapperOptions {\n /**\n * Function to get the current time in milliseconds.\n * Default uses element's ownCurrentTimeMs if available, otherwise 0.\n */\n getTimeMs?: () => number;\n}\n\n/**\n * Create a backwards-compatible frameTask wrapper for a FrameRenderable element.\n * \n * This factory function creates a frameTask object that:\n * - Manages its own AbortController for cancellation\n * - Calls prepareFrame() then renderFrame() in sequence\n * - Silently ignores AbortErrors (expected during cancellation)\n * - Provides taskComplete promise for awaiting completion\n * \n * @param element - The element implementing FrameRenderable\n * @param options - Optional configuration\n * @returns A frameTask object compatible with legacy code\n * \n * @example\n * ```typescript\n * class MyElement extends LitElement implements FrameRenderable {\n * frameTask = createFrameTaskWrapper(this, {\n * getTimeMs: () => this.currentSourceTimeMs,\n * });\n * \n * getFrameState(timeMs: number): FrameState { ... }\n * async prepareFrame(timeMs: number, signal: AbortSignal): Promise<void> { ... }\n * renderFrame(timeMs: number): void { ... }\n * }\n * ```\n */\nexport function createFrameTaskWrapper(\n element: FrameRenderable & { ownCurrentTimeMs?: number; desiredSeekTimeMs?: number },\n options: FrameTaskWrapperOptions = {}\n): FrameTask {\n let frameTaskPromise: Promise<void> = Promise.resolve();\n\n const getTimeMs = options.getTimeMs ?? (() => {\n // Try desiredSeekTimeMs first (video), then ownCurrentTimeMs, then 0\n if (\"desiredSeekTimeMs\" in element && typeof element.desiredSeekTimeMs === \"number\") {\n return element.desiredSeekTimeMs;\n }\n if (\"ownCurrentTimeMs\" in element && typeof element.ownCurrentTimeMs === \"number\") {\n return element.ownCurrentTimeMs;\n }\n return 0;\n });\n\n const taskObj: FrameTask = {\n run: () => {\n const abortController = new AbortController();\n const timeMs = getTimeMs();\n \n frameTaskPromise = (async () => {\n try {\n await element.prepareFrame(timeMs, abortController.signal);\n element.renderFrame(timeMs);\n } catch (error) {\n // Silently ignore AbortErrors - expected when task is cancelled\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n throw error;\n }\n })();\n \n taskObj.taskComplete = frameTaskPromise;\n return frameTaskPromise;\n },\n taskComplete: Promise.resolve(),\n };\n return taskObj;\n}\n"],"mappings":";;;;;AA4BA,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;AAuE9B,SAAgB,kBAAkB,SAA8C;AAC9E,QACE,OAAO,YAAY,YACnB,YAAY,QACZ,mBAAmB,WACnB,kBAAkB,WAClB,iBAAiB,WACjB,OAAQ,QAA4B,kBAAkB,cACtD,OAAQ,QAA4B,iBAAiB,cACrD,OAAQ,QAA4B,gBAAgB;;;;;;AA0BxD,IAAa,kBAAb,MAA6B;CAC3B;CACA,mBAA2C;CAC3C,oBAAoB;CACpB,qBAAoC;CAEpC,YAAY,aAAqD;AAC/D,QAAKA,cAAe;;;;;CAMtB,QAAc;AACZ,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB;;;;;;;;;;;;;;CAe1B,MAAM,YACJ,QACA,UAA8B,EAAE,EACjB;EACf,MAAM,EAAE,mBAAmB,MAAM,uBAAuB;AAGxD,MAAI,MAAKC,kBAAmB;AAC1B,SAAKC,oBAAqB;AAC1B;;AAIF,QAAKF,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;EAC7C,MAAM,SAAS,MAAKA,gBAAiB;AAErC,QAAKC,mBAAoB;AAEzB,MAAI;AAEF,OAAI,kBAAkB;AACpB,UAAM,MAAKF,YAAa;AACxB,WAAO,gBAAgB;;GAKzB,MAAM,WAAW,MAAKI,qBAAsB,OAAO;AACnD,UAAO,gBAAgB;GAGvB,MAAM,6BAA6B,SAAS,QACzC,OAAO,GAAG,cAAc,OAAO,CAAC,iBAClC;AAED,OAAI,2BAA2B,SAAS,GAAG;AACzC,UAAM,QAAQ,IACZ,2BAA2B,KAAK,OAAO,GAAG,aAAa,QAAQ,OAAO,CAAC,CACxE;AACD,WAAO,gBAAgB;;GAIzB,MAAM,iBAAiB,CAAC,GAAG,SAAS,CAAC,MAClC,GAAG,MAAM,EAAE,cAAc,OAAO,CAAC,WAAW,EAAE,cAAc,OAAO,CAAC,SACtE;AAED,QAAK,MAAM,WAAW,gBAAgB;AACpC,WAAO,gBAAgB;AACvB,YAAQ,YAAY,OAAO;;AAI7B,OAAI,mBACF,oBAAmB,MAAKJ,YAAa;YAE/B;AACR,SAAKE,mBAAoB;AAGzB,OAAI,MAAKC,sBAAuB,MAAM;IACpC,MAAM,cAAc,MAAKA;AACzB,UAAKA,oBAAqB;AAE1B,SAAK,YAAY,aAAa,QAAQ,CAAC,YAAY,GAEjD;;;;;;;;;;;;;;;;CAiBR,sBAAsB,QAAmC;EACvD,MAAME,SAA4B,EAAE;EACpC,MAAM,gBAAgB;EAEtB,MAAM,QAAQ,YAA2B;AAKvC,OAFmB,iBAAiB,WAAW,eAAe,SAE9C;IAGd,MAAM,UAAW,QAAqC,eAAe;IACrE,MAAM,QAAS,QAAmC,aAAa;AAG/D,QAAI,EAFwB,iBAAiB,WAAW,gBAAgB,OAItE;AAIF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;UAEjB;AAEL,QAAI,mBAAmB,aAAa;AAElC,SAAI,QAAQ,MAAM,YAAY,OAC5B;KAGF,MAAM,QAAQ,iBAAiB,QAAQ;AACvC,SAAI,MAAM,YAAY,UAAU,MAAM,eAAe,SACnD;;AAKJ,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;;GAKxB,MAAM,WAAW,MAAKC,4BAA6B,QAAQ;AAC3D,QAAK,MAAM,SAAS,SAClB,MAAK,MAAM;;AAIf,OAAK,MAAKN,YAAa;AACvB,SAAO;;;;;;;CAQT,6BAA6B,SAA6B;AAExD,MAAI,QAAQ,YAAY;GACtB,MAAM,QAAQ,QAAQ,WAAW,iBAAiB,OAAO;AACzD,OAAI,MAAM,SAAS,GAAG;IACpB,MAAMO,mBAA8B,EAAE;AACtC,SAAK,MAAM,QAAQ,MACjB,kBAAiB,KAAK,GAAG,KAAK,kBAAkB,CAAC;AAGnD,SAAK,MAAM,SAAS,QAAQ,WAAW,SACrC,KAAI,MAAM,YAAY,OACpB,kBAAiB,KAAK,MAAM;AAGhC,WAAO;;;AAKX,SAAO,MAAM,KAAK,QAAQ,SAAS;;;;;CAMrC,IAAI,cAAuB;AACzB,SAAO,MAAKL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGhB,SAAgB,uBACd,SACA,UAAmC,EAAE,EAC1B;CACX,IAAIM,mBAAkC,QAAQ,SAAS;CAEvD,MAAM,YAAY,QAAQ,oBAAoB;AAE5C,MAAI,uBAAuB,WAAW,OAAO,QAAQ,sBAAsB,SACzE,QAAO,QAAQ;AAEjB,MAAI,sBAAsB,WAAW,OAAO,QAAQ,qBAAqB,SACvE,QAAO,QAAQ;AAEjB,SAAO;;CAGT,MAAMC,UAAqB;EACzB,WAAW;GACT,MAAM,kBAAkB,IAAI,iBAAiB;GAC7C,MAAM,SAAS,WAAW;AAE1B,uBAAoB,YAAY;AAC9B,QAAI;AACF,WAAM,QAAQ,aAAa,QAAQ,gBAAgB,OAAO;AAC1D,aAAQ,YAAY,OAAO;aACpB,OAAO;AAEd,SAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD;AAEF,WAAM;;OAEN;AAEJ,WAAQ,eAAe;AACvB,UAAO;;EAET,cAAc,QAAQ,SAAS;EAChC;AACD,QAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../elements/EFVideo.js";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { LRUCache } from "../utils/LRUCache.js";
|
|
2
|
+
|
|
3
|
+
//#region src/preview/RenderContext.ts
|
|
4
|
+
/**
|
|
5
|
+
* Check if an element has a renderVersion property.
|
|
6
|
+
*/
|
|
7
|
+
function hasRenderVersion(element) {
|
|
8
|
+
return "renderVersion" in element && typeof element.renderVersion === "number";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Module-level counter for generating unique element IDs.
|
|
12
|
+
* This ensures uniqueness across all RenderContext instances.
|
|
13
|
+
*/
|
|
14
|
+
let nextElementId = 1;
|
|
15
|
+
/**
|
|
16
|
+
* WeakMap to store unique IDs for elements.
|
|
17
|
+
* Using WeakMap ensures we don't prevent garbage collection of elements.
|
|
18
|
+
* The ID is stable for the lifetime of the element.
|
|
19
|
+
*/
|
|
20
|
+
const elementUniqueIds = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
/**
|
|
22
|
+
* Get or create a unique ID for an element.
|
|
23
|
+
* This guarantees uniqueness even for elements with the same id attribute
|
|
24
|
+
* or no id attribute at all.
|
|
25
|
+
*/
|
|
26
|
+
function getElementUniqueId(element) {
|
|
27
|
+
let id = elementUniqueIds.get(element);
|
|
28
|
+
if (id === void 0) {
|
|
29
|
+
id = nextElementId++;
|
|
30
|
+
elementUniqueIds.set(element, id);
|
|
31
|
+
}
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* RenderContext provides scoped caching for render operations.
|
|
36
|
+
*
|
|
37
|
+
* Create at the start of a render, dispose when complete:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const context = new RenderContext();
|
|
40
|
+
* try {
|
|
41
|
+
* // ... render operations
|
|
42
|
+
* } finally {
|
|
43
|
+
* context.dispose();
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
var RenderContext = class {
|
|
48
|
+
/** Cache for static element canvases (ef-image, ef-waveform) */
|
|
49
|
+
#canvasCache;
|
|
50
|
+
/** Cache for video frames by source timestamp */
|
|
51
|
+
#videoFrameCache;
|
|
52
|
+
/** Whether this context has been disposed */
|
|
53
|
+
#disposed = false;
|
|
54
|
+
/** Metrics for monitoring cache effectiveness */
|
|
55
|
+
#metrics = {
|
|
56
|
+
canvasCacheHits: 0,
|
|
57
|
+
canvasCacheMisses: 0,
|
|
58
|
+
videoFrameCacheHits: 0,
|
|
59
|
+
videoFrameCacheMisses: 0
|
|
60
|
+
};
|
|
61
|
+
constructor(options = {}) {
|
|
62
|
+
const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;
|
|
63
|
+
this.#canvasCache = new LRUCache(maxCanvasCacheSize);
|
|
64
|
+
this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if the context has been disposed.
|
|
68
|
+
*/
|
|
69
|
+
get disposed() {
|
|
70
|
+
return this.#disposed;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get cache metrics for monitoring.
|
|
74
|
+
*/
|
|
75
|
+
get metrics() {
|
|
76
|
+
return { ...this.#metrics };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate a cache key for a static element.
|
|
80
|
+
* Uses a unique element ID (via WeakMap) to ensure uniqueness even if
|
|
81
|
+
* multiple elements have the same id attribute.
|
|
82
|
+
* Returns null if the element doesn't support caching (no renderVersion).
|
|
83
|
+
*/
|
|
84
|
+
#getCanvasCacheKey(element) {
|
|
85
|
+
if (!hasRenderVersion(element)) return null;
|
|
86
|
+
return `canvas:${getElementUniqueId(element)}:${element.renderVersion}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get a cached dataURL for a static element.
|
|
90
|
+
* Returns undefined if not cached or element doesn't support caching.
|
|
91
|
+
*/
|
|
92
|
+
getCachedCanvasDataUrl(element) {
|
|
93
|
+
if (this.#disposed) return void 0;
|
|
94
|
+
const key = this.#getCanvasCacheKey(element);
|
|
95
|
+
if (!key) return void 0;
|
|
96
|
+
const cached = this.#canvasCache.get(key);
|
|
97
|
+
if (cached) this.#metrics.canvasCacheHits++;
|
|
98
|
+
else this.#metrics.canvasCacheMisses++;
|
|
99
|
+
return cached;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Cache a dataURL for a static element.
|
|
103
|
+
* Does nothing if the element doesn't support caching.
|
|
104
|
+
*/
|
|
105
|
+
setCachedCanvasDataUrl(element, dataUrl) {
|
|
106
|
+
if (this.#disposed) return;
|
|
107
|
+
const key = this.#getCanvasCacheKey(element);
|
|
108
|
+
if (key) this.#canvasCache.set(key, dataUrl);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Generate a cache key for a video frame.
|
|
112
|
+
* Uses a unique element ID (via WeakMap) to ensure uniqueness even if
|
|
113
|
+
* multiple videos have the same id attribute.
|
|
114
|
+
*/
|
|
115
|
+
#getVideoFrameCacheKey(videoElement, sourceTimeMs) {
|
|
116
|
+
return `video:${getElementUniqueId(videoElement)}:${Math.round(sourceTimeMs)}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get a cached video frame.
|
|
120
|
+
* Returns undefined if not cached.
|
|
121
|
+
*/
|
|
122
|
+
getCachedVideoFrame(videoElement, sourceTimeMs) {
|
|
123
|
+
if (this.#disposed) return void 0;
|
|
124
|
+
const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);
|
|
125
|
+
const cached = this.#videoFrameCache.get(key);
|
|
126
|
+
if (cached) this.#metrics.videoFrameCacheHits++;
|
|
127
|
+
else this.#metrics.videoFrameCacheMisses++;
|
|
128
|
+
return cached;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Cache a video frame.
|
|
132
|
+
*/
|
|
133
|
+
setCachedVideoFrame(videoElement, sourceTimeMs, frame) {
|
|
134
|
+
if (this.#disposed) return;
|
|
135
|
+
const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);
|
|
136
|
+
this.#videoFrameCache.set(key, frame);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Convenience method to get or capture a video frame.
|
|
140
|
+
* Checks cache first, then captures if not cached.
|
|
141
|
+
*
|
|
142
|
+
* @param video - The ef-video element
|
|
143
|
+
* @param sourceTimeMs - Source media timestamp
|
|
144
|
+
* @param options - Capture options including quality and signal
|
|
145
|
+
* @returns The captured frame data
|
|
146
|
+
*/
|
|
147
|
+
async getOrCaptureVideoFrame(video, sourceTimeMs, options = {}) {
|
|
148
|
+
const cached = this.getCachedVideoFrame(video, sourceTimeMs);
|
|
149
|
+
if (cached) return cached;
|
|
150
|
+
const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);
|
|
151
|
+
this.setCachedVideoFrame(video, sourceTimeMs, frame);
|
|
152
|
+
return frame;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Dispose the context and clear all caches.
|
|
156
|
+
* Should be called when rendering is complete.
|
|
157
|
+
*/
|
|
158
|
+
dispose() {
|
|
159
|
+
if (this.#disposed) return;
|
|
160
|
+
this.#canvasCache.clear();
|
|
161
|
+
this.#videoFrameCache.clear();
|
|
162
|
+
this.#disposed = true;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Symbol.dispose implementation for use with the `using` keyword.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* using context = new RenderContext();
|
|
170
|
+
* // ... render operations
|
|
171
|
+
* // context is automatically disposed when scope exits
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
[Symbol.dispose]() {
|
|
175
|
+
this.dispose();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the current size of the canvas cache.
|
|
179
|
+
*/
|
|
180
|
+
get canvasCacheSize() {
|
|
181
|
+
return this.#canvasCache.size;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the current size of the video frame cache.
|
|
185
|
+
*/
|
|
186
|
+
get videoFrameCacheSize() {
|
|
187
|
+
return this.#videoFrameCache.size;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
export { RenderContext };
|
|
193
|
+
//# sourceMappingURL=RenderContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenderContext.js","names":["#canvasCache","#videoFrameCache","#disposed","#metrics","#getCanvasCacheKey","#getVideoFrameCacheKey"],"sources":["../../src/preview/RenderContext.ts"],"sourcesContent":["/**\n * RenderContext manages scoped caches for the rendering pipeline.\n * \n * Used during foreignObject serialization to cache:\n * - Video frames by source timestamp (useful for freeze frames, slow-mo)\n * - Static element canvases by element identity + renderVersion\n * \n * The context should be created at the start of a render operation\n * and disposed when the render completes (success or failure).\n */\n\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\n\n/**\n * Check if an element has a renderVersion property.\n */\nfunction hasRenderVersion(element: Element): element is Element & { renderVersion: number } {\n return \"renderVersion\" in element && typeof (element as any).renderVersion === \"number\";\n}\n\n/**\n * Module-level counter for generating unique element IDs.\n * This ensures uniqueness across all RenderContext instances.\n */\nlet nextElementId = 1;\n\n/**\n * WeakMap to store unique IDs for elements.\n * Using WeakMap ensures we don't prevent garbage collection of elements.\n * The ID is stable for the lifetime of the element.\n */\nconst elementUniqueIds = new WeakMap<Element, number>();\n\n/**\n * Get or create a unique ID for an element.\n * This guarantees uniqueness even for elements with the same id attribute\n * or no id attribute at all.\n */\nfunction getElementUniqueId(element: Element): number {\n let id = elementUniqueIds.get(element);\n if (id === undefined) {\n id = nextElementId++;\n elementUniqueIds.set(element, id);\n }\n return id;\n}\n\n/**\n * Result of capturing a video frame.\n */\nexport interface CapturedFrame {\n dataUrl: string;\n width: number;\n height: number;\n}\n\n/**\n * Options for creating a RenderContext.\n */\nexport interface RenderContextOptions {\n /** Maximum number of canvas dataURLs to cache (default: 50) */\n maxCanvasCacheSize?: number;\n /** Maximum number of video frame dataURLs to cache (default: 100) */\n maxVideoFrameCacheSize?: number;\n}\n\n/**\n * RenderContext provides scoped caching for render operations.\n * \n * Create at the start of a render, dispose when complete:\n * ```typescript\n * const context = new RenderContext();\n * try {\n * // ... render operations\n * } finally {\n * context.dispose();\n * }\n * ```\n */\nexport class RenderContext {\n /** Cache for static element canvases (ef-image, ef-waveform) */\n #canvasCache: LRUCache<string, string>;\n \n /** Cache for video frames by source timestamp */\n #videoFrameCache: LRUCache<string, CapturedFrame>;\n \n /** Whether this context has been disposed */\n #disposed = false;\n \n /** Metrics for monitoring cache effectiveness */\n #metrics = {\n canvasCacheHits: 0,\n canvasCacheMisses: 0,\n videoFrameCacheHits: 0,\n videoFrameCacheMisses: 0,\n };\n\n constructor(options: RenderContextOptions = {}) {\n const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;\n this.#canvasCache = new LRUCache(maxCanvasCacheSize);\n this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);\n }\n\n /**\n * Check if the context has been disposed.\n */\n get disposed(): boolean {\n return this.#disposed;\n }\n\n /**\n * Get cache metrics for monitoring.\n */\n get metrics() {\n return { ...this.#metrics };\n }\n\n // ============================================================================\n // Static Element Cache (ef-image, ef-waveform)\n // ============================================================================\n\n /**\n * Generate a cache key for a static element.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple elements have the same id attribute.\n * Returns null if the element doesn't support caching (no renderVersion).\n */\n #getCanvasCacheKey(element: Element): string | null {\n if (!hasRenderVersion(element)) {\n return null;\n }\n // Use unique element ID + render version for guaranteed uniqueness\n const uniqueId = getElementUniqueId(element);\n return `canvas:${uniqueId}:${element.renderVersion}`;\n }\n\n /**\n * Get a cached dataURL for a static element.\n * Returns undefined if not cached or element doesn't support caching.\n */\n getCachedCanvasDataUrl(element: Element): string | undefined {\n if (this.#disposed) return undefined;\n \n const key = this.#getCanvasCacheKey(element);\n if (!key) return undefined;\n \n const cached = this.#canvasCache.get(key);\n if (cached) {\n this.#metrics.canvasCacheHits++;\n } else {\n this.#metrics.canvasCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a dataURL for a static element.\n * Does nothing if the element doesn't support caching.\n */\n setCachedCanvasDataUrl(element: Element, dataUrl: string): void {\n if (this.#disposed) return;\n \n const key = this.#getCanvasCacheKey(element);\n if (key) {\n this.#canvasCache.set(key, dataUrl);\n }\n }\n\n // ============================================================================\n // Video Frame Cache\n // ============================================================================\n\n /**\n * Generate a cache key for a video frame.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple videos have the same id attribute.\n */\n #getVideoFrameCacheKey(videoElement: Element, sourceTimeMs: number): string {\n const uniqueId = getElementUniqueId(videoElement);\n // Round to nearest ms to avoid floating point issues\n const roundedTime = Math.round(sourceTimeMs);\n return `video:${uniqueId}:${roundedTime}`;\n }\n\n /**\n * Get a cached video frame.\n * Returns undefined if not cached.\n */\n getCachedVideoFrame(videoElement: Element, sourceTimeMs: number): CapturedFrame | undefined {\n if (this.#disposed) return undefined;\n \n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n const cached = this.#videoFrameCache.get(key);\n if (cached) {\n this.#metrics.videoFrameCacheHits++;\n } else {\n this.#metrics.videoFrameCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a video frame.\n */\n setCachedVideoFrame(videoElement: Element, sourceTimeMs: number, frame: CapturedFrame): void {\n if (this.#disposed) return;\n \n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n this.#videoFrameCache.set(key, frame);\n }\n\n /**\n * Convenience method to get or capture a video frame.\n * Checks cache first, then captures if not cached.\n * \n * @param video - The ef-video element\n * @param sourceTimeMs - Source media timestamp\n * @param options - Capture options including quality and signal\n * @returns The captured frame data\n */\n async getOrCaptureVideoFrame(\n video: EFVideo,\n sourceTimeMs: number,\n options: {\n quality?: \"auto\" | \"scrub\" | \"main\";\n signal?: AbortSignal;\n } = {}\n ): Promise<CapturedFrame> {\n // Check cache first\n const cached = this.getCachedVideoFrame(video, sourceTimeMs);\n if (cached) {\n return cached;\n }\n\n // Capture frame using direct API\n const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);\n \n // Cache for future use\n this.setCachedVideoFrame(video, sourceTimeMs, frame);\n \n return frame;\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Dispose the context and clear all caches.\n * Should be called when rendering is complete.\n */\n dispose(): void {\n if (this.#disposed) return;\n \n this.#canvasCache.clear();\n this.#videoFrameCache.clear();\n this.#disposed = true;\n }\n\n /**\n * Symbol.dispose implementation for use with the `using` keyword.\n * \n * @example\n * ```typescript\n * using context = new RenderContext();\n * // ... render operations\n * // context is automatically disposed when scope exits\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Get the current size of the canvas cache.\n */\n get canvasCacheSize(): number {\n return this.#canvasCache.size;\n }\n\n /**\n * Get the current size of the video frame cache.\n */\n get videoFrameCacheSize(): number {\n return this.#videoFrameCache.size;\n }\n}\n"],"mappings":";;;;;;AAiBA,SAAS,iBAAiB,SAAkE;AAC1F,QAAO,mBAAmB,WAAW,OAAQ,QAAgB,kBAAkB;;;;;;AAOjF,IAAI,gBAAgB;;;;;;AAOpB,MAAM,mCAAmB,IAAI,SAA0B;;;;;;AAOvD,SAAS,mBAAmB,SAA0B;CACpD,IAAI,KAAK,iBAAiB,IAAI,QAAQ;AACtC,KAAI,OAAO,QAAW;AACpB,OAAK;AACL,mBAAiB,IAAI,SAAS,GAAG;;AAEnC,QAAO;;;;;;;;;;;;;;;AAmCT,IAAa,gBAAb,MAA2B;;CAEzB;;CAGA;;CAGA,YAAY;;CAGZ,WAAW;EACT,iBAAiB;EACjB,mBAAmB;EACnB,qBAAqB;EACrB,uBAAuB;EACxB;CAED,YAAY,UAAgC,EAAE,EAAE;EAC9C,MAAM,EAAE,qBAAqB,IAAI,yBAAyB,QAAQ;AAClE,QAAKA,cAAe,IAAI,SAAS,mBAAmB;AACpD,QAAKC,kBAAmB,IAAI,SAAS,uBAAuB;;;;;CAM9D,IAAI,WAAoB;AACtB,SAAO,MAAKC;;;;;CAMd,IAAI,UAAU;AACZ,SAAO,EAAE,GAAG,MAAKC,SAAU;;;;;;;;CAa7B,mBAAmB,SAAiC;AAClD,MAAI,CAAC,iBAAiB,QAAQ,CAC5B,QAAO;AAIT,SAAO,UADU,mBAAmB,QAAQ,CAClB,GAAG,QAAQ;;;;;;CAOvC,uBAAuB,SAAsC;AAC3D,MAAI,MAAKD,SAAW,QAAO;EAE3B,MAAM,MAAM,MAAKE,kBAAmB,QAAQ;AAC5C,MAAI,CAAC,IAAK,QAAO;EAEjB,MAAM,SAAS,MAAKJ,YAAa,IAAI,IAAI;AACzC,MAAI,OACF,OAAKG,QAAS;MAEd,OAAKA,QAAS;AAEhB,SAAO;;;;;;CAOT,uBAAuB,SAAkB,SAAuB;AAC9D,MAAI,MAAKD,SAAW;EAEpB,MAAM,MAAM,MAAKE,kBAAmB,QAAQ;AAC5C,MAAI,IACF,OAAKJ,YAAa,IAAI,KAAK,QAAQ;;;;;;;CAavC,uBAAuB,cAAuB,cAA8B;AAI1E,SAAO,SAHU,mBAAmB,aAAa,CAGxB,GADL,KAAK,MAAM,aAAa;;;;;;CAQ9C,oBAAoB,cAAuB,cAAiD;AAC1F,MAAI,MAAKE,SAAW,QAAO;EAE3B,MAAM,MAAM,MAAKG,sBAAuB,cAAc,aAAa;EACnE,MAAM,SAAS,MAAKJ,gBAAiB,IAAI,IAAI;AAC7C,MAAI,OACF,OAAKE,QAAS;MAEd,OAAKA,QAAS;AAEhB,SAAO;;;;;CAMT,oBAAoB,cAAuB,cAAsB,OAA4B;AAC3F,MAAI,MAAKD,SAAW;EAEpB,MAAM,MAAM,MAAKG,sBAAuB,cAAc,aAAa;AACnE,QAAKJ,gBAAiB,IAAI,KAAK,MAAM;;;;;;;;;;;CAYvC,MAAM,uBACJ,OACA,cACA,UAGI,EAAE,EACkB;EAExB,MAAM,SAAS,KAAK,oBAAoB,OAAO,aAAa;AAC5D,MAAI,OACF,QAAO;EAIT,MAAM,QAAQ,MAAM,MAAM,yBAAyB,cAAc,QAAQ;AAGzE,OAAK,oBAAoB,OAAO,cAAc,MAAM;AAEpD,SAAO;;;;;;CAWT,UAAgB;AACd,MAAI,MAAKC,SAAW;AAEpB,QAAKF,YAAa,OAAO;AACzB,QAAKC,gBAAiB,OAAO;AAC7B,QAAKC,WAAY;;;;;;;;;;;;CAanB,CAAC,OAAO,WAAiB;AACvB,OAAK,SAAS;;;;;CAMhB,IAAI,kBAA0B;AAC5B,SAAO,MAAKF,YAAa;;;;;CAM3B,IAAI,sBAA8B;AAChC,SAAO,MAAKC,gBAAiB"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
2
|
+
import { WorkerPool } from "../workers/WorkerPool.js";
|
|
3
|
+
import { getEncoderWorkerUrl } from "../workers/encoderWorkerInline.js";
|
|
4
|
+
import { encodeCanvasOnMainThread } from "./mainThreadEncoder.js";
|
|
5
|
+
import { encodeCanvasInWorker } from "./workerEncoder.js";
|
|
6
|
+
|
|
7
|
+
//#region src/preview/encoding/canvasEncoder.ts
|
|
8
|
+
let _workerPool = null;
|
|
9
|
+
let _workerPoolWarningLogged = false;
|
|
10
|
+
/**
|
|
11
|
+
* Get or create the worker pool for canvas encoding.
|
|
12
|
+
* Returns null if workers are not available.
|
|
13
|
+
*/
|
|
14
|
+
function getWorkerPool() {
|
|
15
|
+
if (_workerPool) return _workerPool;
|
|
16
|
+
if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
|
|
17
|
+
if (!_workerPoolWarningLogged) {
|
|
18
|
+
_workerPoolWarningLogged = true;
|
|
19
|
+
logger.warn("[canvasEncoder] Web Workers or OffscreenCanvas not available, using main thread fallback");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
_workerPool = new WorkerPool(getEncoderWorkerUrl());
|
|
25
|
+
if (!_workerPool.isAvailable()) {
|
|
26
|
+
const reason = _workerPool.workerCount === 0 ? "no workers created (check console for errors)" : "workers not available";
|
|
27
|
+
_workerPool = null;
|
|
28
|
+
if (!_workerPoolWarningLogged) {
|
|
29
|
+
_workerPoolWarningLogged = true;
|
|
30
|
+
logger.warn(`[canvasEncoder] Worker pool initialization failed (${reason}), using main thread fallback`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
_workerPool = null;
|
|
35
|
+
if (!_workerPoolWarningLogged) {
|
|
36
|
+
_workerPoolWarningLogged = true;
|
|
37
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
38
|
+
logger.warn(`[canvasEncoder] Failed to create worker pool: ${errorMessage} - using main thread fallback`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return _workerPool;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if an element is an EFVideo.
|
|
45
|
+
*/
|
|
46
|
+
function isEFVideo(element) {
|
|
47
|
+
return element.tagName === "EF-VIDEO";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if an element is an EFSurface.
|
|
51
|
+
*/
|
|
52
|
+
function isEFSurface(element) {
|
|
53
|
+
return element.tagName === "EF-SURFACE";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encode canvases to data URLs in parallel using worker pool.
|
|
57
|
+
* Falls back to main thread encoding if workers are unavailable.
|
|
58
|
+
*
|
|
59
|
+
* When RenderContext and sourceMap are provided:
|
|
60
|
+
* - Checks cache for static elements (ef-image, ef-waveform)
|
|
61
|
+
* - Uses direct capture API for ef-video elements
|
|
62
|
+
* - Shares cached frames for ef-surface elements targeting ef-video
|
|
63
|
+
*
|
|
64
|
+
* @param canvases - Array of canvases to encode
|
|
65
|
+
* @param options - Encoding options including optional renderContext and sourceMap
|
|
66
|
+
* @returns Promise resolving to array of encoded results
|
|
67
|
+
*/
|
|
68
|
+
async function encodeCanvasesInParallel(canvases, options = {}) {
|
|
69
|
+
const { scale: canvasScale = 1, renderContext, sourceMap } = options;
|
|
70
|
+
const workerPool = getWorkerPool();
|
|
71
|
+
const encodeCanvas = async (canvas) => {
|
|
72
|
+
try {
|
|
73
|
+
if (canvas.width === 0 || canvas.height === 0) return null;
|
|
74
|
+
const preserveAlpha = canvas.dataset.preserveAlpha === "true";
|
|
75
|
+
const sourceElement = sourceMap?.get(canvas);
|
|
76
|
+
if (renderContext && sourceElement) {
|
|
77
|
+
if (isEFVideo(sourceElement)) {
|
|
78
|
+
const sourceTimeMs = sourceElement.currentSourceTimeMs;
|
|
79
|
+
try {
|
|
80
|
+
return {
|
|
81
|
+
canvas,
|
|
82
|
+
dataUrl: (await sourceElement.captureFrameAtSourceTime(sourceTimeMs, { quality: "main" })).dataUrl,
|
|
83
|
+
preserveAlpha: false
|
|
84
|
+
};
|
|
85
|
+
} catch (e) {
|
|
86
|
+
logger.warn("[canvasEncoder] Direct capture failed, falling back to canvas encoding:", e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (isEFSurface(sourceElement)) {
|
|
90
|
+
const target = sourceElement.targetElement;
|
|
91
|
+
if (target && isEFVideo(target)) {
|
|
92
|
+
const videoTarget = target;
|
|
93
|
+
const sourceTimeMs = videoTarget.currentSourceTimeMs;
|
|
94
|
+
const cached = renderContext.getCachedVideoFrame(videoTarget, sourceTimeMs);
|
|
95
|
+
if (cached) return {
|
|
96
|
+
canvas,
|
|
97
|
+
dataUrl: cached.dataUrl,
|
|
98
|
+
preserveAlpha: false
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
const frame = await videoTarget.captureFrameAtSourceTime(sourceTimeMs, { quality: "main" });
|
|
102
|
+
renderContext.setCachedVideoFrame(videoTarget, sourceTimeMs, frame);
|
|
103
|
+
return {
|
|
104
|
+
canvas,
|
|
105
|
+
dataUrl: frame.dataUrl,
|
|
106
|
+
preserveAlpha: false
|
|
107
|
+
};
|
|
108
|
+
} catch (e) {
|
|
109
|
+
logger.warn("[canvasEncoder] Direct capture for surface target failed:", e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const cachedDataUrl = renderContext.getCachedCanvasDataUrl(sourceElement);
|
|
114
|
+
if (cachedDataUrl) return {
|
|
115
|
+
canvas,
|
|
116
|
+
dataUrl: cachedDataUrl,
|
|
117
|
+
preserveAlpha
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let sourceCanvas = canvas;
|
|
121
|
+
if (canvasScale < 1) {
|
|
122
|
+
const scaledWidth = Math.floor(canvas.width * canvasScale);
|
|
123
|
+
const scaledHeight = Math.floor(canvas.height * canvasScale);
|
|
124
|
+
const scaledCanvas = document.createElement("canvas");
|
|
125
|
+
scaledCanvas.width = scaledWidth;
|
|
126
|
+
scaledCanvas.height = scaledHeight;
|
|
127
|
+
const scaledCtx = scaledCanvas.getContext("2d");
|
|
128
|
+
if (scaledCtx) {
|
|
129
|
+
scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);
|
|
130
|
+
sourceCanvas = scaledCanvas;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let dataUrl;
|
|
134
|
+
if (workerPool) dataUrl = await workerPool.execute((worker) => encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha));
|
|
135
|
+
else {
|
|
136
|
+
const encoded = encodeCanvasOnMainThread(sourceCanvas, canvasScale);
|
|
137
|
+
if (!encoded) return null;
|
|
138
|
+
dataUrl = encoded.dataUrl;
|
|
139
|
+
}
|
|
140
|
+
if (renderContext && sourceElement) renderContext.setCachedCanvasDataUrl(sourceElement, dataUrl);
|
|
141
|
+
return {
|
|
142
|
+
canvas,
|
|
143
|
+
dataUrl,
|
|
144
|
+
preserveAlpha
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.warn("[canvasEncoder] Worker encoding failed, using main thread fallback:", error);
|
|
148
|
+
const encoded = encodeCanvasOnMainThread(canvas, canvasScale);
|
|
149
|
+
if (encoded) {
|
|
150
|
+
logger.warn("[canvasEncoder] Main thread fallback succeeded");
|
|
151
|
+
return {
|
|
152
|
+
canvas,
|
|
153
|
+
...encoded
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
logger.warn("[canvasEncoder] Main thread encoding also failed, skipping canvas:", error);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const encodingTasks = canvases.map(encodeCanvas);
|
|
161
|
+
return (await Promise.all(encodingTasks)).filter((r) => r !== null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { encodeCanvasesInParallel };
|
|
166
|
+
//# sourceMappingURL=canvasEncoder.js.map
|