@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EF_FRAMEGEN.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +16 -6
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +129 -56
- package/dist/elements/EFThumbnailStrip.js +605 -359
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +233 -25
- package/dist/elements/EFTimegroup.js +865 -144
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +154 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +7 -1
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +178 -0
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +833 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/preview/workers/encoderWorkerInline.js +103 -0
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +71 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -1,165 +1,97 @@
|
|
|
1
1
|
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
|
|
2
|
-
import { OrderedLRUCache } from "../utils/LRUCache.js";
|
|
3
2
|
import { TargetController } from "./TargetController.js";
|
|
4
|
-
import {
|
|
3
|
+
import { findRootTemporal } from "./findRootTemporal.js";
|
|
4
|
+
import { timelineStateContext } from "../gui/timeline/timelineStateContext.js";
|
|
5
|
+
import { getCacheKey, sessionThumbnailCache } from "./SessionThumbnailCache.js";
|
|
6
|
+
import { consume } from "@lit/context";
|
|
5
7
|
import { LitElement, css, html } from "lit";
|
|
6
8
|
import { customElement, property, state } from "lit/decorators.js";
|
|
7
9
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
8
10
|
|
|
9
11
|
//#region src/elements/EFThumbnailStrip.ts
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* Uses OrderedLRUCache for efficient timestamp-based searching
|
|
14
|
-
*/
|
|
15
|
-
const thumbnailImageCache = new OrderedLRUCache(200, (a, b) => {
|
|
16
|
-
const partsA = a.split(":");
|
|
17
|
-
const partsB = b.split(":");
|
|
18
|
-
return Number.parseFloat(partsA[partsA.length - 1] || "0") - Number.parseFloat(partsB[partsB.length - 1] || "0");
|
|
19
|
-
});
|
|
20
|
-
globalThis.debugThumbnailCache = thumbnailImageCache;
|
|
21
|
-
/**
|
|
22
|
-
* Quantize timestamp to 30fps frame boundaries for consistent caching
|
|
23
|
-
* This eliminates cache misses from floating point precision differences
|
|
24
|
-
*/
|
|
25
|
-
function quantizeTimestamp(timeMs) {
|
|
26
|
-
const frameIntervalMs = 1e3 / 30;
|
|
27
|
-
return Math.round(timeMs / frameIntervalMs) * frameIntervalMs;
|
|
12
|
+
/** Type guard to check if element is EFVideo */
|
|
13
|
+
function isEFVideo(element) {
|
|
14
|
+
return element?.tagName.toLowerCase() === "ef-video";
|
|
28
15
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function getThumbnailCacheKey(videoSrc, timeMs) {
|
|
33
|
-
return `${videoSrc}:${quantizeTimestamp(timeMs)}`;
|
|
16
|
+
/** Type guard to check if element is EFTimegroup */
|
|
17
|
+
function isEFTimegroup(element) {
|
|
18
|
+
return element?.tagName.toLowerCase() === "ef-timegroup";
|
|
34
19
|
}
|
|
35
|
-
const THUMBNAIL_GAP = 1;
|
|
36
|
-
const STRIP_BORDER_PADDING = 4;
|
|
37
20
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
21
|
+
* Get identifiers for cache key generation.
|
|
22
|
+
* Returns rootId (for cache isolation), elementId (for element-specific caching), and epoch (for content versioning).
|
|
40
23
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const thumbnailPitch = thumbnailWidth + THUMBNAIL_GAP;
|
|
47
|
-
const baseFitCount = Math.floor(stripWidth / thumbnailPitch);
|
|
48
|
-
const count = Math.max(1, baseFitCount + 1);
|
|
49
|
-
const timestamps = [];
|
|
50
|
-
const timeRange = endTimeMs - startTimeMs;
|
|
51
|
-
for (let i = 0; i < count; i++) {
|
|
52
|
-
const timeMs = count === 1 ? (startTimeMs + endTimeMs) / 2 : startTimeMs + i * timeRange / (count - 1);
|
|
53
|
-
timestamps.push(timeMs);
|
|
54
|
-
}
|
|
55
|
-
const segmentMap = /* @__PURE__ */ new Map();
|
|
56
|
-
for (const timeMs of timestamps) {
|
|
57
|
-
const segmentId = scrubSegmentDurationMs ? Math.floor(timeMs / scrubSegmentDurationMs) : 0;
|
|
58
|
-
if (!segmentMap.has(segmentId)) segmentMap.set(segmentId, []);
|
|
59
|
-
segmentMap.get(segmentId).push({ timeMs });
|
|
60
|
-
}
|
|
24
|
+
function getCacheIdentifiers(element) {
|
|
25
|
+
const rootTemporal = findRootTemporal(element);
|
|
26
|
+
const rootTimegroup = rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null;
|
|
27
|
+
const rootId = rootTimegroup?.id || "default";
|
|
28
|
+
const epoch = rootTimegroup?.contentEpoch ?? 0;
|
|
61
29
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
thumbnails
|
|
66
|
-
}))
|
|
30
|
+
rootId,
|
|
31
|
+
elementId: isEFVideo(element) ? element.src || element.id || "video" : element.id || "timegroup",
|
|
32
|
+
epoch
|
|
67
33
|
};
|
|
68
34
|
}
|
|
35
|
+
/** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */
|
|
36
|
+
const VIRTUAL_RENDER_PADDING_PX = 200;
|
|
37
|
+
/** Default gap between thumbnails */
|
|
38
|
+
const DEFAULT_GAP = 4;
|
|
39
|
+
/** Default aspect ratio if unknown */
|
|
40
|
+
const DEFAULT_ASPECT_RATIO = 16 / 9;
|
|
41
|
+
/** Max canvas width for thumbnail captures */
|
|
42
|
+
const MAX_CAPTURE_WIDTH = 480;
|
|
43
|
+
/** Thumbnails to capture per batch */
|
|
44
|
+
const BATCH_SIZE = 10;
|
|
69
45
|
let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
70
46
|
constructor(..._args) {
|
|
71
47
|
super(..._args);
|
|
72
48
|
this.canvasRef = createRef();
|
|
73
|
-
this._targetController = new TargetController(this);
|
|
74
|
-
this._targetElement = null;
|
|
75
49
|
this.target = "";
|
|
76
|
-
this.thumbnailWidth =
|
|
50
|
+
this.thumbnailWidth = 0;
|
|
51
|
+
this.gap = DEFAULT_GAP;
|
|
77
52
|
this.useIntrinsicDuration = false;
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
segments: []
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
return this.calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine);
|
|
105
|
-
},
|
|
106
|
-
args: () => [
|
|
107
|
-
this.stripWidth,
|
|
108
|
-
this.thumbnailWidth,
|
|
109
|
-
this.targetElement,
|
|
110
|
-
this.startTimeMs,
|
|
111
|
-
this.endTimeMs,
|
|
112
|
-
this.useIntrinsicDuration,
|
|
113
|
-
this.targetElement?.mediaEngineTask?.value
|
|
114
|
-
]
|
|
115
|
-
});
|
|
116
|
-
this.thumbnailRenderTask = new Task(this, {
|
|
117
|
-
autoRun: false,
|
|
118
|
-
task: async ([layout, targetElement, thumbnailWidth]) => {
|
|
119
|
-
if (!layout || !targetElement) return [];
|
|
120
|
-
return this.renderThumbnails(layout, targetElement, thumbnailWidth);
|
|
121
|
-
},
|
|
122
|
-
args: () => [
|
|
123
|
-
this.thumbnailLayoutTask.value || null,
|
|
124
|
-
this.targetElement,
|
|
125
|
-
this.thumbnailWidth
|
|
126
|
-
]
|
|
127
|
-
});
|
|
53
|
+
this.pixelsPerMs = .1;
|
|
54
|
+
this._targetController = new TargetController(this);
|
|
55
|
+
this._targetElement = null;
|
|
56
|
+
this._width = 0;
|
|
57
|
+
this._height = 0;
|
|
58
|
+
this._scrollContainer = null;
|
|
59
|
+
this._currentScrollLeft = 0;
|
|
60
|
+
this._trackLeftOffset = 0;
|
|
61
|
+
this._thumbnailSlots = [];
|
|
62
|
+
this._captureInProgress = false;
|
|
63
|
+
this._renderRequested = false;
|
|
64
|
+
this._hasLoadedThumbnails = false;
|
|
65
|
+
this._lastLoadedEpoch = null;
|
|
66
|
+
this._lastLayoutParams = null;
|
|
67
|
+
this._onScroll = () => {
|
|
68
|
+
if (!this._scrollContainer) return;
|
|
69
|
+
this._currentScrollLeft = this._scrollContainer.scrollLeft;
|
|
70
|
+
this._drawCanvas();
|
|
71
|
+
if (!this._scrollFrame) this._scrollFrame = requestAnimationFrame(() => {
|
|
72
|
+
this._scrollFrame = void 0;
|
|
73
|
+
this._loadVisibleThumbnails();
|
|
74
|
+
});
|
|
75
|
+
};
|
|
128
76
|
}
|
|
129
77
|
static {
|
|
130
78
|
this.styles = [css`
|
|
131
79
|
:host {
|
|
132
80
|
display: block;
|
|
133
81
|
position: relative;
|
|
134
|
-
|
|
135
|
-
height: 48px; /* Default filmstrip height */
|
|
136
|
-
background: #2a2a2a;
|
|
137
|
-
border: 2px solid #333;
|
|
138
|
-
border-radius: 6px;
|
|
82
|
+
background: #1a1a2e;
|
|
139
83
|
overflow: hidden;
|
|
140
|
-
|
|
84
|
+
width: 100%;
|
|
85
|
+
height: 100%;
|
|
141
86
|
}
|
|
142
87
|
canvas {
|
|
143
88
|
display: block;
|
|
89
|
+
/* Absolute positioning - we manually position at visible region */
|
|
144
90
|
position: absolute;
|
|
145
91
|
top: 0;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
.loading-overlay {
|
|
151
|
-
position: absolute;
|
|
152
|
-
top: 0;
|
|
153
|
-
left: 0;
|
|
154
|
-
right: 0;
|
|
155
|
-
bottom: 0;
|
|
156
|
-
background: rgba(42, 42, 42, 0.9);
|
|
157
|
-
display: flex;
|
|
158
|
-
align-items: center;
|
|
159
|
-
justify-content: center;
|
|
160
|
-
font-size: 11px;
|
|
161
|
-
color: #ccc;
|
|
162
|
-
font-weight: 500;
|
|
92
|
+
/* Left and width set programmatically based on visible portion */
|
|
93
|
+
height: 100%;
|
|
94
|
+
image-rendering: auto;
|
|
163
95
|
}
|
|
164
96
|
`];
|
|
165
97
|
}
|
|
@@ -169,20 +101,166 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
169
101
|
set targetElement(value) {
|
|
170
102
|
const oldValue = this._targetElement;
|
|
171
103
|
this._targetElement = value;
|
|
172
|
-
this.
|
|
173
|
-
if (value
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
104
|
+
this._mutationObserver?.disconnect();
|
|
105
|
+
if (value !== oldValue) {
|
|
106
|
+
this._hasLoadedThumbnails = false;
|
|
107
|
+
this._lastLoadedEpoch = null;
|
|
108
|
+
this._lastLayoutParams = null;
|
|
109
|
+
}
|
|
110
|
+
if (value && value !== oldValue) this._setupTargetObserver(value);
|
|
111
|
+
this.requestUpdate("targetElement", oldValue);
|
|
112
|
+
}
|
|
113
|
+
connectedCallback() {
|
|
114
|
+
super.connectedCallback();
|
|
115
|
+
this._resizeObserver = new ResizeObserver((entries) => {
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const box = entry.borderBoxSize?.[0];
|
|
118
|
+
this._width = box?.inlineSize ?? entry.contentRect.width;
|
|
119
|
+
this._height = box?.blockSize ?? entry.contentRect.height;
|
|
120
|
+
this._calculateTrackOffset();
|
|
121
|
+
this._scheduleRender();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this._resizeObserver.observe(this);
|
|
125
|
+
this.updateComplete.then(() => {
|
|
126
|
+
this._findScrollContainer();
|
|
127
|
+
this._scheduleRender();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
disconnectedCallback() {
|
|
131
|
+
super.disconnectedCallback();
|
|
132
|
+
this._resizeObserver?.disconnect();
|
|
133
|
+
this._mutationObserver?.disconnect();
|
|
134
|
+
this._detachScrollListener();
|
|
135
|
+
if (this._scrollFrame) cancelAnimationFrame(this._scrollFrame);
|
|
136
|
+
}
|
|
137
|
+
updated(changedProperties) {
|
|
138
|
+
super.updated(changedProperties);
|
|
139
|
+
if (changedProperties.has("thumbnailWidth") || changedProperties.has("gap") || changedProperties.has("startTimeMs") || changedProperties.has("endTimeMs") || changedProperties.has("useIntrinsicDuration") || changedProperties.has("pixelsPerMs") || changedProperties.has("targetElement")) this._scheduleRender();
|
|
140
|
+
if (changedProperties.has("_timelineState")) this._onContextScroll();
|
|
141
|
+
}
|
|
142
|
+
_findScrollContainer() {
|
|
143
|
+
let node = this.parentNode;
|
|
144
|
+
while (node) {
|
|
145
|
+
if (node instanceof HTMLElement) {
|
|
146
|
+
const style = getComputedStyle(node);
|
|
147
|
+
if (style.overflowX === "auto" || style.overflowX === "scroll") {
|
|
148
|
+
this._scrollContainer = node;
|
|
149
|
+
this._calculateTrackOffset();
|
|
150
|
+
this._attachScrollListener();
|
|
151
|
+
return;
|
|
182
152
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
153
|
+
}
|
|
154
|
+
if (node.parentNode) node = node.parentNode;
|
|
155
|
+
else if (node instanceof ShadowRoot) node = node.host;
|
|
156
|
+
else break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Calculate the horizontal offset from scroll container's left edge to this element's track.
|
|
161
|
+
* This accounts for sticky labels or other elements that precede the track area.
|
|
162
|
+
*
|
|
163
|
+
* We look for our specific timeline elements (ef-timeline-row) and measure their label width.
|
|
164
|
+
*/
|
|
165
|
+
_calculateTrackOffset() {
|
|
166
|
+
if (!this._scrollContainer) {
|
|
167
|
+
this._trackLeftOffset = 0;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const timelineRow = this._findTimelineRow();
|
|
171
|
+
if (timelineRow) {
|
|
172
|
+
const labelWidth = this._getTimelineRowLabelWidth(timelineRow);
|
|
173
|
+
if (labelWidth > 0) {
|
|
174
|
+
this._trackLeftOffset = labelWidth;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this._trackLeftOffset = 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Find the ef-timeline-row ancestor by walking up through shadow DOM boundaries.
|
|
182
|
+
*/
|
|
183
|
+
_findTimelineRow() {
|
|
184
|
+
let node = this;
|
|
185
|
+
while (node) {
|
|
186
|
+
if (node instanceof Element && node.tagName.toLowerCase() === "ef-timeline-row") return node;
|
|
187
|
+
const parentNode = node.parentNode;
|
|
188
|
+
if (parentNode instanceof ShadowRoot) node = parentNode.host;
|
|
189
|
+
else node = parentNode;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the label width from an ef-timeline-row element.
|
|
195
|
+
* Queries the shadow root for .row-label and returns its width.
|
|
196
|
+
*/
|
|
197
|
+
_getTimelineRowLabelWidth(timelineRow) {
|
|
198
|
+
const shadowRoot = timelineRow.shadowRoot;
|
|
199
|
+
if (!shadowRoot) return 0;
|
|
200
|
+
const rowLabel = shadowRoot.querySelector(".row-label");
|
|
201
|
+
if (!rowLabel) return 0;
|
|
202
|
+
return rowLabel.getBoundingClientRect().width;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get this strip's absolute position in the timeline (pixels from timeline origin).
|
|
206
|
+
* Uses the target element's startTimeMs to determine position.
|
|
207
|
+
*/
|
|
208
|
+
_getStripTimelinePosition() {
|
|
209
|
+
const target = this._targetElement;
|
|
210
|
+
if (!target) return 0;
|
|
211
|
+
if (isEFVideo(target)) return (target.startTimeMs ?? 0) * (this._timelineState?.pixelsPerMs ?? .1);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
_attachScrollListener() {
|
|
215
|
+
if (!this._scrollContainer) return;
|
|
216
|
+
this._scrollContainer.addEventListener("scroll", this._onScroll, { passive: true });
|
|
217
|
+
this._currentScrollLeft = this._scrollContainer.scrollLeft;
|
|
218
|
+
}
|
|
219
|
+
_detachScrollListener() {
|
|
220
|
+
if (this._scrollContainer) {
|
|
221
|
+
this._scrollContainer.removeEventListener("scroll", this._onScroll);
|
|
222
|
+
this._scrollContainer = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
_onContextScroll() {
|
|
226
|
+
if (!this._timelineState || this._scrollContainer) return;
|
|
227
|
+
this._currentScrollLeft = this._timelineState.viewportScrollLeft;
|
|
228
|
+
this._drawCanvas();
|
|
229
|
+
}
|
|
230
|
+
get _viewportWidth() {
|
|
231
|
+
if (this._timelineState?.viewportWidth) return this._timelineState.viewportWidth;
|
|
232
|
+
if (this._scrollContainer) return this._scrollContainer.clientWidth - this._trackLeftOffset;
|
|
233
|
+
return this._width;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Watch for async content loading from child media elements.
|
|
237
|
+
* When media finishes loading, increment the epoch to invalidate cached thumbnails.
|
|
238
|
+
*/
|
|
239
|
+
_watchChildContentLoading(target) {
|
|
240
|
+
const mediaElements = target.querySelectorAll("ef-video, ef-image, ef-audio");
|
|
241
|
+
for (const el of mediaElements) {
|
|
242
|
+
const mediaEngine = el.mediaEngineTask;
|
|
243
|
+
if (mediaEngine?.taskComplete) mediaEngine.taskComplete.then(() => {
|
|
244
|
+
if (this._targetElement === target) {
|
|
245
|
+
target.incrementContentEpoch();
|
|
246
|
+
this._lastLayoutParams = null;
|
|
247
|
+
this._scheduleRender();
|
|
248
|
+
}
|
|
249
|
+
}).catch(() => {});
|
|
250
|
+
const fetchTask = el.fetchImage;
|
|
251
|
+
if (fetchTask?.taskComplete) fetchTask.taskComplete.then(() => {
|
|
252
|
+
if (this._targetElement === target) {
|
|
253
|
+
target.incrementContentEpoch();
|
|
254
|
+
this._lastLayoutParams = null;
|
|
255
|
+
this._scheduleRender();
|
|
256
|
+
}
|
|
257
|
+
}).catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
_setupTargetObserver(target) {
|
|
261
|
+
if (isEFVideo(target)) {
|
|
262
|
+
this._mutationObserver = new MutationObserver(() => this._scheduleRender());
|
|
263
|
+
this._mutationObserver.observe(target, {
|
|
186
264
|
attributes: true,
|
|
187
265
|
attributeFilter: [
|
|
188
266
|
"trimstart",
|
|
@@ -192,271 +270,434 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
192
270
|
"src"
|
|
193
271
|
]
|
|
194
272
|
});
|
|
195
|
-
|
|
196
|
-
if (this.
|
|
197
|
-
|
|
273
|
+
target.updateComplete.then(() => {
|
|
274
|
+
if (this._targetElement !== target) return;
|
|
275
|
+
target.mediaEngineTask?.taskComplete.then(() => {
|
|
276
|
+
if (this._targetElement !== target) return;
|
|
277
|
+
this._scheduleRender();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
} else if (isEFTimegroup(target)) {
|
|
281
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
282
|
+
if (mutations.some((mutation) => {
|
|
283
|
+
if (mutation.type === "childList") return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;
|
|
284
|
+
if (mutation.type === "attributes") {
|
|
285
|
+
const attrName = mutation.attributeName;
|
|
286
|
+
if (attrName === "currenttime" || attrName === "current-time" || attrName === "playing" || attrName === "loop") return false;
|
|
287
|
+
return attrName === "src" || attrName === "asset-id" || attrName === "style" || attrName === "transform";
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
})) {
|
|
291
|
+
const epochBefore = target.contentEpoch;
|
|
292
|
+
target.incrementContentEpoch();
|
|
293
|
+
if (target.contentEpoch !== epochBefore) {
|
|
294
|
+
this._lastLayoutParams = null;
|
|
295
|
+
if (mutations.some((m) => m.addedNodes.length > 0)) this._watchChildContentLoading(target);
|
|
296
|
+
this._scheduleRender();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
this._mutationObserver.observe(target, {
|
|
301
|
+
childList: true,
|
|
302
|
+
subtree: true,
|
|
303
|
+
attributes: true,
|
|
304
|
+
attributeFilter: [
|
|
305
|
+
"src",
|
|
306
|
+
"asset-id",
|
|
307
|
+
"style",
|
|
308
|
+
"transform"
|
|
309
|
+
]
|
|
310
|
+
});
|
|
311
|
+
this._watchChildContentLoading(target);
|
|
312
|
+
if (target.durationMs === 0) {
|
|
313
|
+
const checkDuration = () => {
|
|
314
|
+
if (this._targetElement !== target) return;
|
|
315
|
+
if (target.durationMs > 0) this._scheduleRender();
|
|
316
|
+
else requestAnimationFrame(checkDuration);
|
|
317
|
+
};
|
|
318
|
+
requestAnimationFrame(checkDuration);
|
|
319
|
+
}
|
|
198
320
|
}
|
|
199
|
-
this.requestUpdate("targetElement", oldValue);
|
|
200
321
|
}
|
|
201
|
-
|
|
202
|
-
if (this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}).finally(() => {
|
|
212
|
-
this._thumbnailLayoutTask = void 0;
|
|
213
|
-
if (this._pendingStripWidth) {
|
|
214
|
-
this.stripWidth = this._pendingStripWidth;
|
|
215
|
-
this._pendingStripWidth = void 0;
|
|
216
|
-
}
|
|
322
|
+
_scheduleRender() {
|
|
323
|
+
if (this._renderRequested) return;
|
|
324
|
+
this._renderRequested = true;
|
|
325
|
+
requestAnimationFrame(() => {
|
|
326
|
+
this._renderRequested = false;
|
|
327
|
+
this._calculateLayout();
|
|
328
|
+
this._checkCache();
|
|
329
|
+
this._drawCanvas();
|
|
330
|
+
this._loadVisibleThumbnails();
|
|
331
|
+
this._checkAndDispatchReady();
|
|
217
332
|
});
|
|
218
333
|
}
|
|
219
|
-
get stripWidth() {
|
|
220
|
-
return this._stripWidth;
|
|
221
|
-
}
|
|
222
334
|
/**
|
|
223
|
-
*
|
|
335
|
+
* Check if thumbnails are ready and dispatch event if not already done.
|
|
224
336
|
*/
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (width > 0) this.stripWidth = width;
|
|
337
|
+
_checkAndDispatchReady() {
|
|
338
|
+
if (this._hasLoadedThumbnails) return;
|
|
339
|
+
const hasLayout = this._thumbnailSlots.length > 0;
|
|
340
|
+
const hasAnyCached = this._thumbnailSlots.some((s) => s.status === "cached");
|
|
341
|
+
const hasPending = this._thumbnailSlots.some((s) => s.status === "pending");
|
|
342
|
+
if (hasLayout && (hasAnyCached || !hasPending)) {
|
|
343
|
+
this._hasLoadedThumbnails = true;
|
|
344
|
+
this.dispatchEvent(new CustomEvent("thumbnails-ready", { bubbles: true }));
|
|
234
345
|
}
|
|
235
|
-
if (changedProperties.has("thumbnailWidth") || changedProperties.has("startTimeMs") || changedProperties.has("endTimeMs") || changedProperties.has("useIntrinsicDuration")) this.runThumbnailUpdate();
|
|
236
346
|
}
|
|
237
347
|
/**
|
|
238
|
-
*
|
|
348
|
+
* Calculate thumbnail layout based on current dimensions and time range.
|
|
349
|
+
* Only recreates slots if layout parameters have actually changed.
|
|
239
350
|
*/
|
|
240
|
-
|
|
241
|
-
if (this.
|
|
242
|
-
this.
|
|
351
|
+
_calculateLayout() {
|
|
352
|
+
if (this._width <= 0 || this._height <= 0 || !this._targetElement) {
|
|
353
|
+
this._thumbnailSlots = [];
|
|
354
|
+
this._lastLayoutParams = null;
|
|
243
355
|
return;
|
|
244
356
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
357
|
+
const timeRange = this._getTimeRange();
|
|
358
|
+
if (timeRange.endMs <= timeRange.startMs) {
|
|
359
|
+
this._thumbnailSlots = [];
|
|
360
|
+
this._lastLayoutParams = null;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const thumbWidth = this._getEffectiveThumbnailWidth();
|
|
364
|
+
const gap = this.gap;
|
|
365
|
+
const currentParams = {
|
|
366
|
+
width: this._width,
|
|
367
|
+
height: this._height,
|
|
368
|
+
startTimeMs: timeRange.startMs,
|
|
369
|
+
endTimeMs: timeRange.endMs,
|
|
370
|
+
thumbWidth,
|
|
371
|
+
gap
|
|
372
|
+
};
|
|
373
|
+
if (this._lastLayoutParams && this._lastLayoutParams.width === currentParams.width && this._lastLayoutParams.height === currentParams.height && this._lastLayoutParams.startTimeMs === currentParams.startTimeMs && this._lastLayoutParams.endTimeMs === currentParams.endTimeMs && this._lastLayoutParams.thumbWidth === currentParams.thumbWidth && this._lastLayoutParams.gap === currentParams.gap) return;
|
|
374
|
+
this._lastLayoutParams = currentParams;
|
|
375
|
+
const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));
|
|
376
|
+
const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;
|
|
377
|
+
const slots = [];
|
|
378
|
+
const duration = timeRange.endMs - timeRange.startMs;
|
|
379
|
+
for (let i = 0; i < count; i++) {
|
|
380
|
+
const timeMs = count === 1 ? (timeRange.startMs + timeRange.endMs) / 2 : timeRange.startMs + i * duration / (count - 1);
|
|
381
|
+
slots.push({
|
|
382
|
+
timeMs,
|
|
383
|
+
x: Math.round(i * pitch),
|
|
384
|
+
width: thumbWidth,
|
|
385
|
+
status: "pending"
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
this._thumbnailSlots = slots;
|
|
257
389
|
}
|
|
258
390
|
/**
|
|
259
|
-
*
|
|
391
|
+
* Get effective time range for thumbnails.
|
|
260
392
|
*/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
393
|
+
_getTimeRange() {
|
|
394
|
+
const target = this._targetElement;
|
|
395
|
+
if (!target) return {
|
|
396
|
+
startMs: 0,
|
|
397
|
+
endMs: 0
|
|
398
|
+
};
|
|
399
|
+
if (isEFVideo(target)) {
|
|
400
|
+
if (this.useIntrinsicDuration) return {
|
|
401
|
+
startMs: this.startTimeMs ?? 0,
|
|
402
|
+
endMs: this.endTimeMs ?? target.intrinsicDurationMs ?? 0
|
|
403
|
+
};
|
|
404
|
+
const sourceStart = target.sourceStartMs ?? 0;
|
|
405
|
+
const trimmedDuration = target.durationMs ?? 0;
|
|
406
|
+
return {
|
|
407
|
+
startMs: this.startTimeMs !== void 0 ? sourceStart + this.startTimeMs : sourceStart,
|
|
408
|
+
endMs: this.endTimeMs !== void 0 ? sourceStart + this.endTimeMs : sourceStart + trimmedDuration
|
|
409
|
+
};
|
|
266
410
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return this.generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine);
|
|
411
|
+
return {
|
|
412
|
+
startMs: this.startTimeMs ?? 0,
|
|
413
|
+
endMs: this.endTimeMs && this.endTimeMs > 0 ? this.endTimeMs : target.durationMs ?? 0
|
|
414
|
+
};
|
|
272
415
|
}
|
|
273
416
|
/**
|
|
274
|
-
*
|
|
417
|
+
* Calculate effective thumbnail width (auto or specified).
|
|
275
418
|
*/
|
|
276
|
-
|
|
277
|
-
|
|
419
|
+
_getEffectiveThumbnailWidth() {
|
|
420
|
+
if (this.thumbnailWidth > 0) return this.thumbnailWidth;
|
|
421
|
+
const target = this._targetElement;
|
|
422
|
+
let aspectRatio = DEFAULT_ASPECT_RATIO;
|
|
423
|
+
if (isEFVideo(target)) aspectRatio = (target.videoWidth || 1920) / (target.videoHeight || 1080);
|
|
424
|
+
else if (isEFTimegroup(target)) aspectRatio = (target.offsetWidth || 1920) / (target.offsetHeight || 1080);
|
|
425
|
+
return Math.round(this._height * aspectRatio);
|
|
278
426
|
}
|
|
279
427
|
/**
|
|
280
|
-
*
|
|
428
|
+
* Check cache for existing thumbnails.
|
|
281
429
|
*/
|
|
282
|
-
|
|
283
|
-
if (!
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
let status = "exact-hit";
|
|
292
|
-
let nearHitKey;
|
|
293
|
-
if (!imageData) {
|
|
294
|
-
const timeMinus = Math.max(0, thumbnail.timeMs - 5e3);
|
|
295
|
-
const timePlus = thumbnail.timeMs + 5e3;
|
|
296
|
-
const rangeStartKey = `${videoSrc}:${timeMinus}`;
|
|
297
|
-
const rangeEndKey = `${videoSrc}:${timePlus}`;
|
|
298
|
-
const sameVideoHits = thumbnailImageCache.findRange(rangeStartKey, rangeEndKey).filter((hit) => hit.key.startsWith(`${videoSrc}:`));
|
|
299
|
-
if (sameVideoHits.length > 0) {
|
|
300
|
-
const nearestHit = sameVideoHits.reduce((closest, current) => {
|
|
301
|
-
const currentParts = current.key.split(":");
|
|
302
|
-
const closestParts = closest.key.split(":");
|
|
303
|
-
const currentTime = Number.parseFloat(currentParts[currentParts.length - 1] || "0");
|
|
304
|
-
const closestTime = Number.parseFloat(closestParts[closestParts.length - 1] || "0");
|
|
305
|
-
return Math.abs(currentTime - thumbnail.timeMs) < Math.abs(closestTime - thumbnail.timeMs) ? current : closest;
|
|
306
|
-
});
|
|
307
|
-
imageData = nearestHit.value;
|
|
308
|
-
status = "near-hit";
|
|
309
|
-
nearHitKey = nearestHit.key;
|
|
310
|
-
} else status = "missing";
|
|
311
|
-
}
|
|
312
|
-
const x = thumbnailIndex * (thumbnailWidth + THUMBNAIL_GAP);
|
|
313
|
-
allThumbnails.push({
|
|
314
|
-
timeMs: thumbnail.timeMs,
|
|
315
|
-
segmentId: segment.segmentId,
|
|
316
|
-
x,
|
|
317
|
-
width: thumbnailWidth,
|
|
318
|
-
height: availableHeight,
|
|
319
|
-
status,
|
|
320
|
-
imageData,
|
|
321
|
-
nearHitKey
|
|
322
|
-
});
|
|
323
|
-
thumbnailIndex++;
|
|
430
|
+
_checkCache() {
|
|
431
|
+
if (!this._targetElement) return;
|
|
432
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(this._targetElement);
|
|
433
|
+
for (const slot of this._thumbnailSlots) {
|
|
434
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
435
|
+
if (sessionThumbnailCache.has(key)) {
|
|
436
|
+
slot.imageData = sessionThumbnailCache.get(key);
|
|
437
|
+
slot.status = "cached";
|
|
438
|
+
} else slot.status = "pending";
|
|
324
439
|
}
|
|
325
|
-
await this.drawThumbnails(allThumbnails);
|
|
326
|
-
await this.loadMissingThumbnails(allThumbnails, targetElement);
|
|
327
|
-
return allThumbnails;
|
|
328
|
-
}
|
|
329
|
-
connectedCallback() {
|
|
330
|
-
super.connectedCallback();
|
|
331
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
332
|
-
for (const entry of entries) {
|
|
333
|
-
const width = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.inlineSize : entry.contentRect.width;
|
|
334
|
-
this._stripHeight = (entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.blockSize : entry.contentRect.height) ?? 0;
|
|
335
|
-
this.stripWidth = width ?? 0;
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
this.resizeObserver.observe(this);
|
|
339
|
-
this.updateComplete.then(() => {
|
|
340
|
-
if (this._stripWidth === 0) {
|
|
341
|
-
const width = this.clientWidth;
|
|
342
|
-
if (width > 0) this.stripWidth = width ?? 0;
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
disconnectedCallback() {
|
|
347
|
-
super.disconnectedCallback();
|
|
348
|
-
this.resizeObserver?.disconnect();
|
|
349
|
-
this.resizeObserver = void 0;
|
|
350
|
-
this._videoPropertyObserver?.disconnect();
|
|
351
|
-
this._videoPropertyObserver = void 0;
|
|
352
440
|
}
|
|
353
441
|
/**
|
|
354
|
-
* Draw
|
|
442
|
+
* Draw the canvas with current thumbnail state.
|
|
443
|
+
* Canvas is absolutely positioned at the visible portion of the strip.
|
|
444
|
+
* Uses virtual rendering - only draws thumbnails in the visible region.
|
|
355
445
|
*/
|
|
356
|
-
|
|
446
|
+
_drawCanvas() {
|
|
357
447
|
const canvas = this.canvasRef.value;
|
|
358
448
|
if (!canvas) return;
|
|
359
|
-
const ctx = canvas.getContext("2d");
|
|
449
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
360
450
|
if (!ctx) return;
|
|
451
|
+
const stripWidth = this._width;
|
|
452
|
+
const height = this._height;
|
|
453
|
+
if (stripWidth <= 0 || height <= 0) return;
|
|
361
454
|
const dpr = window.devicePixelRatio || 1;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
455
|
+
const scrollLeft = this._currentScrollLeft;
|
|
456
|
+
const viewportWidth = this._viewportWidth;
|
|
457
|
+
const stripStartPx = this._getStripTimelinePosition();
|
|
458
|
+
const stripEndPx = stripStartPx + stripWidth;
|
|
459
|
+
const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;
|
|
460
|
+
const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;
|
|
461
|
+
if (stripEndPx < visibleLeftPx || stripStartPx > visibleRightPx) {
|
|
462
|
+
canvas.style.display = "none";
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
canvas.style.display = "block";
|
|
466
|
+
const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);
|
|
467
|
+
const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);
|
|
468
|
+
const visibleWidthPx = visibleEndInStrip - visibleStartInStrip;
|
|
469
|
+
if (visibleWidthPx <= 0) {
|
|
470
|
+
canvas.style.display = "none";
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const targetWidth = Math.ceil(visibleWidthPx * dpr);
|
|
474
|
+
const targetHeight = Math.ceil(height * dpr);
|
|
475
|
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
476
|
+
canvas.width = targetWidth;
|
|
477
|
+
canvas.height = targetHeight;
|
|
478
|
+
}
|
|
479
|
+
canvas.style.left = `${visibleStartInStrip}px`;
|
|
480
|
+
canvas.style.width = `${visibleWidthPx}px`;
|
|
481
|
+
canvas.style.height = `${height}px`;
|
|
482
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
483
|
+
ctx.fillStyle = "#1a1a2e";
|
|
484
|
+
ctx.fillRect(0, 0, visibleWidthPx, height);
|
|
485
|
+
for (const slot of this._thumbnailSlots) {
|
|
486
|
+
if (slot.x + slot.width < visibleStartInStrip || slot.x > visibleEndInStrip) continue;
|
|
487
|
+
const drawX = slot.x - visibleStartInStrip;
|
|
488
|
+
if (drawX + slot.width < 0 || drawX > visibleWidthPx) continue;
|
|
489
|
+
if (slot.imageData) this._drawThumbnailImage(ctx, slot.imageData, drawX, slot.width, height);
|
|
490
|
+
else {
|
|
491
|
+
ctx.fillStyle = slot.status === "loading" ? "#2d2d50" : "#2d2d44";
|
|
492
|
+
ctx.fillRect(drawX, 0, slot.width, height);
|
|
493
|
+
if (slot.status === "loading") {
|
|
494
|
+
ctx.fillStyle = "rgba(59, 130, 246, 0.3)";
|
|
495
|
+
ctx.fillRect(drawX, 0, slot.width, 2);
|
|
496
|
+
}
|
|
397
497
|
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Draw a thumbnail image with cover mode scaling.
|
|
502
|
+
*/
|
|
503
|
+
_drawThumbnailImage(ctx, imageData, x, width, height) {
|
|
504
|
+
const tempCanvas = document.createElement("canvas");
|
|
505
|
+
tempCanvas.width = imageData.width;
|
|
506
|
+
tempCanvas.height = imageData.height;
|
|
507
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
508
|
+
if (!tempCtx) return;
|
|
509
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
510
|
+
const srcAspect = imageData.width / imageData.height;
|
|
511
|
+
const dstAspect = width / height;
|
|
512
|
+
let srcX = 0, srcY = 0, srcW = imageData.width, srcH = imageData.height;
|
|
513
|
+
if (srcAspect > dstAspect) {
|
|
514
|
+
srcW = imageData.height * dstAspect;
|
|
515
|
+
srcX = (imageData.width - srcW) / 2;
|
|
398
516
|
} else {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
517
|
+
srcH = imageData.width / dstAspect;
|
|
518
|
+
srcY = (imageData.height - srcH) / 2;
|
|
519
|
+
}
|
|
520
|
+
ctx.drawImage(tempCanvas, srcX, srcY, srcW, srcH, x, 0, width, height);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Load thumbnails that are visible in the current viewport.
|
|
524
|
+
* Skips loading if the epoch hasn't changed since last load.
|
|
525
|
+
*/
|
|
526
|
+
async _loadVisibleThumbnails() {
|
|
527
|
+
if (this._captureInProgress || !this._targetElement) return;
|
|
528
|
+
if (isEFTimegroup(this._targetElement)) {
|
|
529
|
+
const currentEpoch = this._targetElement.contentEpoch;
|
|
530
|
+
if (this._lastLoadedEpoch !== null && this._lastLoadedEpoch === currentEpoch) {
|
|
531
|
+
if (!this._thumbnailSlots.some((s) => s.status === "pending")) return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const viewportWidth = this._viewportWidth;
|
|
535
|
+
const scrollOffset = this._currentScrollLeft;
|
|
536
|
+
const stripWidth = this._width;
|
|
537
|
+
const stripStartPx = this._getStripTimelinePosition();
|
|
538
|
+
const visibleLeftPx = scrollOffset - VIRTUAL_RENDER_PADDING_PX;
|
|
539
|
+
const visibleRightPx = scrollOffset + viewportWidth + VIRTUAL_RENDER_PADDING_PX;
|
|
540
|
+
const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);
|
|
541
|
+
const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);
|
|
542
|
+
const pending = this._thumbnailSlots.filter((slot) => {
|
|
543
|
+
if (slot.status !== "pending") return false;
|
|
544
|
+
return slot.x + slot.width >= visibleStartInStrip && slot.x <= visibleEndInStrip;
|
|
545
|
+
});
|
|
546
|
+
if (pending.length === 0) return;
|
|
547
|
+
this._captureInProgress = true;
|
|
548
|
+
for (const slot of pending) slot.status = "loading";
|
|
549
|
+
this._drawCanvas();
|
|
550
|
+
try {
|
|
551
|
+
if (isEFTimegroup(this._targetElement)) await this._captureTimegroupThumbnails(pending);
|
|
552
|
+
else if (isEFVideo(this._targetElement)) await this._captureVideoThumbnails(pending);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.warn("Failed to capture thumbnails:", error);
|
|
555
|
+
for (const slot of pending) if (slot.status === "loading") slot.status = "pending";
|
|
556
|
+
} finally {
|
|
557
|
+
this._captureInProgress = false;
|
|
558
|
+
this._drawCanvas();
|
|
559
|
+
if (isEFTimegroup(this._targetElement)) this._lastLoadedEpoch = this._targetElement.contentEpoch;
|
|
560
|
+
if (this._thumbnailSlots.some((s) => s.status === "cached") && !this._hasLoadedThumbnails) {
|
|
561
|
+
this._hasLoadedThumbnails = true;
|
|
562
|
+
this.dispatchEvent(new CustomEvent("thumbnails-ready", { bubbles: true }));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Capture thumbnails from a timegroup target.
|
|
568
|
+
*/
|
|
569
|
+
async _captureTimegroupThumbnails(slots) {
|
|
570
|
+
const target = this._targetElement;
|
|
571
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(target);
|
|
572
|
+
const timegroupWidth = target.offsetWidth || 1920;
|
|
573
|
+
const timegroupHeight = target.offsetHeight || 1080;
|
|
574
|
+
const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);
|
|
575
|
+
for (let i = 0; i < slots.length; i += BATCH_SIZE) {
|
|
576
|
+
const batch = slots.slice(i, i + BATCH_SIZE);
|
|
577
|
+
const timestamps = batch.map((s) => s.timeMs);
|
|
578
|
+
try {
|
|
579
|
+
const canvases = await target.captureBatch(timestamps, {
|
|
580
|
+
scale,
|
|
581
|
+
contentReadyMode: "immediate"
|
|
582
|
+
});
|
|
583
|
+
for (let j = 0; j < batch.length; j++) {
|
|
584
|
+
const slot = batch[j];
|
|
585
|
+
const canvas = canvases[j];
|
|
586
|
+
if (canvas) {
|
|
587
|
+
const imageData = this._canvasToImageData(canvas);
|
|
588
|
+
if (imageData) {
|
|
589
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
590
|
+
sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
|
|
591
|
+
slot.imageData = imageData;
|
|
592
|
+
slot.status = "cached";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
this._drawCanvas();
|
|
597
|
+
if (i + BATCH_SIZE < slots.length) await new Promise((r) => requestAnimationFrame(r));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.warn("Batch capture failed:", error);
|
|
600
|
+
}
|
|
407
601
|
}
|
|
408
602
|
}
|
|
409
603
|
/**
|
|
410
|
-
*
|
|
604
|
+
* Capture thumbnails from a video target using MediaEngine.
|
|
411
605
|
*/
|
|
412
|
-
async
|
|
413
|
-
const
|
|
606
|
+
async _captureVideoThumbnails(slots) {
|
|
607
|
+
const target = this._targetElement;
|
|
608
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(target);
|
|
609
|
+
if (target.mediaEngineTask) await target.mediaEngineTask.taskComplete;
|
|
610
|
+
const mediaEngine = target.mediaEngineTask?.value;
|
|
414
611
|
if (!mediaEngine) return;
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const timestamps =
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
612
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
613
|
+
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
614
|
+
if (!videoRendition && !scrubRendition) return;
|
|
615
|
+
const timestamps = slots.map((s) => s.timeMs);
|
|
616
|
+
const abortController = new AbortController();
|
|
617
|
+
try {
|
|
618
|
+
const results = await mediaEngine.extractThumbnails(timestamps, abortController.signal);
|
|
619
|
+
for (let i = 0; i < slots.length; i++) {
|
|
620
|
+
const slot = slots[i];
|
|
621
|
+
const result = results[i];
|
|
622
|
+
if (result?.thumbnail) {
|
|
623
|
+
const imageData = this._canvasToImageData(result.thumbnail);
|
|
624
|
+
if (imageData) {
|
|
625
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
626
|
+
sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
|
|
627
|
+
slot.imageData = imageData;
|
|
628
|
+
slot.status = "cached";
|
|
629
|
+
}
|
|
430
630
|
}
|
|
431
631
|
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
abortController.abort();
|
|
634
|
+
console.warn("Video thumbnail extraction failed:", error);
|
|
432
635
|
}
|
|
433
|
-
await this.drawThumbnails(thumbnails);
|
|
434
636
|
}
|
|
435
637
|
/**
|
|
436
|
-
* Convert
|
|
638
|
+
* Convert canvas to ImageData.
|
|
437
639
|
*/
|
|
438
|
-
|
|
439
|
-
const ctx = canvas.getContext("2d");
|
|
640
|
+
_canvasToImageData(canvas) {
|
|
641
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
440
642
|
if (!ctx) return null;
|
|
441
643
|
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
442
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Returns a promise that resolves when thumbnails are ready.
|
|
647
|
+
* Resolves immediately if thumbnails are already loaded.
|
|
648
|
+
*/
|
|
649
|
+
whenReady() {
|
|
650
|
+
if (this._hasLoadedThumbnails) return Promise.resolve();
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
this.addEventListener("thumbnails-ready", () => resolve(), { once: true });
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Check if thumbnails have been loaded.
|
|
657
|
+
*/
|
|
658
|
+
get isReady() {
|
|
659
|
+
return this._hasLoadedThumbnails;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Invalidate cached thumbnails for this element within a time range.
|
|
663
|
+
* Call this when content changes at specific times.
|
|
664
|
+
*/
|
|
665
|
+
invalidateTimeRange(startTimeMs, endTimeMs) {
|
|
666
|
+
if (!this._targetElement) return;
|
|
667
|
+
const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
|
|
668
|
+
sessionThumbnailCache.invalidateTimeRange(rootId, elementId, startTimeMs, endTimeMs);
|
|
669
|
+
for (const slot of this._thumbnailSlots) if (slot.timeMs >= startTimeMs && slot.timeMs <= endTimeMs) {
|
|
670
|
+
slot.imageData = void 0;
|
|
671
|
+
slot.status = "pending";
|
|
672
|
+
}
|
|
673
|
+
this._scheduleRender();
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Invalidate all cached thumbnails for this element.
|
|
677
|
+
*/
|
|
678
|
+
invalidateAll() {
|
|
679
|
+
if (!this._targetElement) return;
|
|
680
|
+
const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
|
|
681
|
+
sessionThumbnailCache.invalidateElement(rootId, elementId);
|
|
682
|
+
for (const slot of this._thumbnailSlots) {
|
|
683
|
+
slot.imageData = void 0;
|
|
684
|
+
slot.status = "pending";
|
|
685
|
+
}
|
|
686
|
+
this._scheduleRender();
|
|
687
|
+
}
|
|
443
688
|
render() {
|
|
444
|
-
return html
|
|
445
|
-
<canvas ${ref(this.canvasRef)}></canvas>
|
|
446
|
-
${this.thumbnailRenderTask.render({
|
|
447
|
-
pending: () => html``,
|
|
448
|
-
complete: () => html``,
|
|
449
|
-
error: (e) => html`<div class="error">Error loading thumbnails: ${e}</div>`
|
|
450
|
-
})}
|
|
451
|
-
`;
|
|
689
|
+
return html`<canvas ${ref(this.canvasRef)}></canvas>`;
|
|
452
690
|
}
|
|
453
691
|
};
|
|
454
|
-
__decorate([state()], EFThumbnailStrip.prototype, "targetElement", null);
|
|
455
692
|
__decorate([property({ type: String })], EFThumbnailStrip.prototype, "target", void 0);
|
|
456
693
|
__decorate([property({
|
|
457
694
|
type: Number,
|
|
458
695
|
attribute: "thumbnail-width"
|
|
459
696
|
})], EFThumbnailStrip.prototype, "thumbnailWidth", void 0);
|
|
697
|
+
__decorate([property({
|
|
698
|
+
type: Number,
|
|
699
|
+
attribute: "gap"
|
|
700
|
+
})], EFThumbnailStrip.prototype, "gap", void 0);
|
|
460
701
|
__decorate([property({
|
|
461
702
|
type: Number,
|
|
462
703
|
attribute: "start-time-ms"
|
|
@@ -470,14 +711,19 @@ __decorate([property({
|
|
|
470
711
|
attribute: "use-intrinsic-duration",
|
|
471
712
|
reflect: true,
|
|
472
713
|
converter: {
|
|
473
|
-
fromAttribute: (value) =>
|
|
474
|
-
if (value === null) return false;
|
|
475
|
-
return value === "true";
|
|
476
|
-
},
|
|
714
|
+
fromAttribute: (value) => value === "true",
|
|
477
715
|
toAttribute: (value) => value ? "true" : null
|
|
478
716
|
}
|
|
479
717
|
})], EFThumbnailStrip.prototype, "useIntrinsicDuration", void 0);
|
|
480
|
-
__decorate([
|
|
718
|
+
__decorate([property({
|
|
719
|
+
type: Number,
|
|
720
|
+
attribute: "pixels-per-ms"
|
|
721
|
+
})], EFThumbnailStrip.prototype, "pixelsPerMs", void 0);
|
|
722
|
+
__decorate([consume({
|
|
723
|
+
context: timelineStateContext,
|
|
724
|
+
subscribe: true
|
|
725
|
+
}), state()], EFThumbnailStrip.prototype, "_timelineState", void 0);
|
|
726
|
+
__decorate([state()], EFThumbnailStrip.prototype, "targetElement", null);
|
|
481
727
|
EFThumbnailStrip = __decorate([customElement("ef-thumbnail-strip")], EFThumbnailStrip);
|
|
482
728
|
|
|
483
729
|
//#endregion
|