@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statsTrackingStrategy.js","names":[],"sources":["../../src/preview/statsTrackingStrategy.ts"],"sourcesContent":["/**\n * Stats tracking strategy for different presentation modes.\n * \n * Uses strategy pattern to encapsulate mode-specific stats tracking logic,\n * allowing each mode to report what stats it supports and provide its own implementation.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\nimport type { CanvasPreviewResult } from \"./renderTimegroupToCanvas.js\";\nimport type { PreviewPresentationMode } from \"./previewSettings.js\";\n\n/**\n * Stat types that can be tracked.\n */\nexport type StatType =\n | \"fps\"\n | \"renderTime\"\n | \"headroom\"\n | \"resolution\"\n | \"resolutionScale\"\n | \"cpuPressure\"\n | \"adaptiveResolution\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null; // null if not measurable\n headroom: number | null; // null if not applicable\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null; // null if not applicable\n samplesAtCurrentScale?: number; // only for adaptive resolution\n canScaleUp?: boolean; // only for adaptive resolution\n canScaleDown?: boolean; // only for adaptive resolution\n}\n\n/**\n * Strategy interface for tracking stats in different presentation modes.\n */\nexport interface StatsTrackingStrategy {\n /** Start tracking stats (called when mode is initialized and stats are enabled) */\n start(): void;\n /** Stop tracking stats (called when mode stops or stats are disabled) */\n stop(): void;\n /** Get current stats, or null if not available */\n getStats(): PlaybackStats | null;\n /** Check if this strategy supports a specific stat type */\n supportsStat(stat: StatType): boolean;\n}\n\n/**\n * Canvas mode stats tracking strategy.\n * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.\n */\nexport class CanvasStatsStrategy implements StatsTrackingStrategy {\n private canvasPreviewResult: CanvasPreviewResult;\n private adaptiveTracker: AdaptiveResolutionTracker;\n private readonly compositionWidth: number;\n private readonly compositionHeight: number;\n private getResolutionScale: () => number;\n private isAtRest: () => boolean;\n private isExporting: () => boolean;\n \n private animationFrame: number | null = null;\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n constructor(options: {\n canvasPreviewResult: CanvasPreviewResult;\n adaptiveTracker: AdaptiveResolutionTracker;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale: () => number;\n isAtRest: () => boolean;\n isExporting: () => boolean;\n }) {\n this.canvasPreviewResult = options.canvasPreviewResult;\n this.adaptiveTracker = options.adaptiveTracker;\n this.compositionWidth = options.compositionWidth;\n this.compositionHeight = options.compositionHeight;\n this.getResolutionScale = options.getResolutionScale;\n this.isAtRest = options.isAtRest;\n this.isExporting = options.isExporting;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n const { refresh } = this.canvasPreviewResult;\n \n const loop = async (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Skip refresh during export to avoid wasting CPU\n if (!this.isExporting()) {\n try {\n // Measure actual render time\n const renderStart = performance.now();\n await refresh();\n const renderTime = performance.now() - renderStart;\n \n // Only record frame timing when in motion (playing/scrubbing)\n // This prevents inflated stats at rest and focuses tracking on actual playback\n if (!this.isAtRest()) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n\n // Update playback stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n // Get CURRENT resolution from the canvas result (may have changed dynamically)\n const currentScale = this.getResolutionScale();\n const renderWidth = Math.floor(this.compositionWidth * currentScale);\n const renderHeight = Math.floor(this.compositionHeight * currentScale);\n this.updateStats(renderWidth, renderHeight, currentScale);\n }\n } catch (e) {\n console.error(\"Canvas stats tracking failed:\", e);\n }\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(_stat: StatType): boolean {\n // Canvas mode supports all stats\n return true;\n }\n\n private updateStats(renderWidth: number, renderHeight: number, resolutionScale: number): void {\n const trackerStats = this.adaptiveTracker.getStats();\n\n this.currentStats = {\n fps: trackerStats.fps,\n avgRenderTime: trackerStats.avgRenderTime,\n headroom: trackerStats.headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n}\n\n/**\n * DOM mode stats tracking strategy.\n * Tracks FPS, resolution, CPU pressure, and frame seek time.\n */\nexport class DomStatsStrategy implements StatsTrackingStrategy {\n private timegroup: EFTimegroup;\n private adaptiveTracker: AdaptiveResolutionTracker;\n \n private animationFrame: number | null = null;\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n \n // Frame seek time tracking\n private seekStartTime = 0;\n private seekTimes: number[] = [];\n private frameTaskCleanup: (() => void) | null = null;\n \n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n }) {\n this.timegroup = options.timegroup;\n this.adaptiveTracker = options.adaptiveTracker;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n // Track frame seek times using frameTask callback\n // This measures how long it takes for the timegroup to fully update after a seek\n const frameTaskCallback = async () => {\n if (this.seekStartTime > 0) {\n const seekTime = performance.now() - this.seekStartTime;\n this.seekTimes.push(seekTime);\n if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.seekTimes.shift();\n }\n this.seekStartTime = 0; // Reset after recording\n }\n };\n \n this.timegroup.addFrameTask(frameTaskCallback);\n this.frameTaskCleanup = () => {\n // Note: EFTimegroup doesn't have removeFrameTask, but this is fine\n // The callback will be cleaned up when the timegroup is destroyed\n };\n \n // Track currentTimeMs changes to detect seeks\n let lastCurrentTimeMs = this.timegroup.currentTimeMs;\n const checkSeek = () => {\n const currentTimeMs = this.timegroup.currentTimeMs;\n if (currentTimeMs !== lastCurrentTimeMs) {\n // Seek detected - start timing\n this.seekStartTime = performance.now();\n lastCurrentTimeMs = currentTimeMs;\n }\n };\n \n const loop = (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Check for seeks\n checkSeek();\n \n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n \n // Update stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n this.updateStats();\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n if (this.frameTaskCleanup) {\n this.frameTaskCleanup();\n this.frameTaskCleanup = null;\n }\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.seekStartTime = 0;\n this.seekTimes = [];\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(stat: StatType): boolean {\n // DOM mode supports: fps, resolution, cpuPressure, renderTime (seek time), headroom\n // Does NOT support: resolutionScale, adaptiveResolution\n return stat === \"fps\" || stat === \"resolution\" || stat === \"cpuPressure\" || stat === \"renderTime\" || stat === \"headroom\";\n }\n\n private updateStats(): void {\n // Calculate FPS from frame intervals\n const avgFrameInterval = this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n \n // Calculate average seek time (frame update time)\n const avgSeekTime = this.seekTimes.length > 0\n ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length\n : 0;\n \n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom = avgSeekTime > 0\n ? this.TARGET_FRAME_TIME_MS - avgSeekTime\n : 0;\n \n // Get CPU pressure from adaptive tracker\n const trackerStats = this.adaptiveTracker.getStats();\n \n // Calculate displayed resolution from timegroup bounding rect\n const rect = this.timegroup.getBoundingClientRect();\n const renderWidth = Math.round(rect.width);\n const renderHeight = Math.round(rect.height);\n\n this.currentStats = {\n fps,\n avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,\n headroom: avgSeekTime > 0 ? headroom : null,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale: null, // Not applicable in DOM mode\n };\n }\n}\n\n/**\n * Factory function to create the appropriate stats tracking strategy for a presentation mode.\n * Returns null for modes that don't support stats tracking.\n */\nexport function createStatsTrackingStrategy(\n mode: PreviewPresentationMode,\n options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n canvasPreviewResult?: CanvasPreviewResult | null;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale?: () => number;\n isAtRest?: () => boolean;\n isExporting?: () => boolean;\n }\n): StatsTrackingStrategy | null {\n switch (mode) {\n case \"canvas\":\n if (!options.canvasPreviewResult || !options.getResolutionScale || !options.isAtRest || !options.isExporting) {\n return null;\n }\n return new CanvasStatsStrategy({\n canvasPreviewResult: options.canvasPreviewResult,\n adaptiveTracker: options.adaptiveTracker,\n compositionWidth: options.compositionWidth,\n compositionHeight: options.compositionHeight,\n getResolutionScale: options.getResolutionScale,\n isAtRest: options.isAtRest,\n isExporting: options.isExporting,\n });\n \n case \"dom\":\n case \"original\": // \"dom\" maps to \"original\" mode\n return new DomStatsStrategy({\n timegroup: options.timegroup,\n adaptiveTracker: options.adaptiveTracker,\n });\n \n case \"clone\":\n case \"computed\": // \"clone\" maps to \"computed\" mode\n // These modes don't support stats tracking\n return null;\n \n default:\n return null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statsTrackingStrategy.js","names":[],"sources":["../../src/preview/statsTrackingStrategy.ts"],"sourcesContent":["/**\n * Stats tracking strategy for different presentation modes.\n * \n * Uses strategy pattern to encapsulate mode-specific stats tracking logic,\n * allowing each mode to report what stats it supports and provide its own implementation.\n */\n\nimport { logger } from \"./logger.js\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\nimport type { CanvasPreviewResult } from \"./renderTimegroupToCanvas.js\";\nimport type { PreviewPresentationMode } from \"./previewSettings.js\";\n\n/**\n * Stat types that can be tracked.\n */\nexport type StatType =\n | \"fps\"\n | \"renderTime\"\n | \"headroom\"\n | \"resolution\"\n | \"resolutionScale\"\n | \"cpuPressure\"\n | \"adaptiveResolution\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null; // null if not measurable\n headroom: number | null; // null if not applicable\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null; // null if not applicable\n samplesAtCurrentScale?: number; // only for adaptive resolution\n canScaleUp?: boolean; // only for adaptive resolution\n canScaleDown?: boolean; // only for adaptive resolution\n}\n\n/**\n * Strategy interface for tracking stats in different presentation modes.\n */\nexport interface StatsTrackingStrategy {\n /** Start tracking stats (called when mode is initialized and stats are enabled) */\n start(): void;\n /** Stop tracking stats (called when mode stops or stats are disabled) */\n stop(): void;\n /** Get current stats, or null if not available */\n getStats(): PlaybackStats | null;\n /** Check if this strategy supports a specific stat type */\n supportsStat(stat: StatType): boolean;\n}\n\n/**\n * Canvas mode stats tracking strategy.\n * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.\n */\nexport class CanvasStatsStrategy implements StatsTrackingStrategy {\n private canvasPreviewResult: CanvasPreviewResult;\n private adaptiveTracker: AdaptiveResolutionTracker;\n private readonly compositionWidth: number;\n private readonly compositionHeight: number;\n private getResolutionScale: () => number;\n private isAtRest: () => boolean;\n private isExporting: () => boolean;\n \n private animationFrame: number | null = null;\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n constructor(options: {\n canvasPreviewResult: CanvasPreviewResult;\n adaptiveTracker: AdaptiveResolutionTracker;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale: () => number;\n isAtRest: () => boolean;\n isExporting: () => boolean;\n }) {\n this.canvasPreviewResult = options.canvasPreviewResult;\n this.adaptiveTracker = options.adaptiveTracker;\n this.compositionWidth = options.compositionWidth;\n this.compositionHeight = options.compositionHeight;\n this.getResolutionScale = options.getResolutionScale;\n this.isAtRest = options.isAtRest;\n this.isExporting = options.isExporting;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n const { refresh } = this.canvasPreviewResult;\n \n const loop = async (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Skip refresh during export to avoid wasting CPU\n if (!this.isExporting()) {\n try {\n // Measure actual render time\n const renderStart = performance.now();\n await refresh();\n const renderTime = performance.now() - renderStart;\n \n // Only record frame timing when in motion (playing/scrubbing)\n // This prevents inflated stats at rest and focuses tracking on actual playback\n if (!this.isAtRest()) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n\n // Update playback stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n // Get CURRENT resolution from the canvas result (may have changed dynamically)\n const currentScale = this.getResolutionScale();\n const renderWidth = Math.floor(this.compositionWidth * currentScale);\n const renderHeight = Math.floor(this.compositionHeight * currentScale);\n this.updateStats(renderWidth, renderHeight, currentScale);\n }\n } catch (e) {\n logger.error(\"Canvas stats tracking failed:\", e);\n }\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(_stat: StatType): boolean {\n // Canvas mode supports all stats\n return true;\n }\n\n private updateStats(renderWidth: number, renderHeight: number, resolutionScale: number): void {\n const trackerStats = this.adaptiveTracker.getStats();\n\n this.currentStats = {\n fps: trackerStats.fps,\n avgRenderTime: trackerStats.avgRenderTime,\n headroom: trackerStats.headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n}\n\n/**\n * DOM mode stats tracking strategy.\n * Tracks FPS, resolution, CPU pressure, and frame seek time.\n */\nexport class DomStatsStrategy implements StatsTrackingStrategy {\n private timegroup: EFTimegroup;\n private adaptiveTracker: AdaptiveResolutionTracker;\n \n private animationFrame: number | null = null;\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n \n // Frame seek time tracking\n private seekStartTime = 0;\n private seekTimes: number[] = [];\n private frameTaskCleanup: (() => void) | null = null;\n \n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n }) {\n this.timegroup = options.timegroup;\n this.adaptiveTracker = options.adaptiveTracker;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n \n // Track frame seek times using frameTask callback\n // This measures how long it takes for the timegroup to fully update after a seek\n const frameTaskCallback = async () => {\n if (this.seekStartTime > 0) {\n const seekTime = performance.now() - this.seekStartTime;\n this.seekTimes.push(seekTime);\n if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.seekTimes.shift();\n }\n this.seekStartTime = 0; // Reset after recording\n }\n };\n \n this.timegroup.addFrameTask(frameTaskCallback);\n this.frameTaskCleanup = () => {\n // Note: EFTimegroup doesn't have removeFrameTask, but this is fine\n // The callback will be cleaned up when the timegroup is destroyed\n };\n \n // Track currentTimeMs changes to detect seeks\n let lastCurrentTimeMs = this.timegroup.currentTimeMs;\n const checkSeek = () => {\n const currentTimeMs = this.timegroup.currentTimeMs;\n if (currentTimeMs !== lastCurrentTimeMs) {\n // Seek detected - start timing\n this.seekStartTime = performance.now();\n lastCurrentTimeMs = currentTimeMs;\n }\n };\n \n const loop = (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n \n // Check for seeks\n checkSeek();\n \n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n \n // Update stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n this.updateStats();\n }\n \n this.animationFrame = requestAnimationFrame(loop);\n };\n \n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n if (this.frameTaskCleanup) {\n this.frameTaskCleanup();\n this.frameTaskCleanup = null;\n }\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.seekStartTime = 0;\n this.seekTimes = [];\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(stat: StatType): boolean {\n // DOM mode supports: fps, resolution, cpuPressure, renderTime (seek time), headroom\n // Does NOT support: resolutionScale, adaptiveResolution\n return stat === \"fps\" || stat === \"resolution\" || stat === \"cpuPressure\" || stat === \"renderTime\" || stat === \"headroom\";\n }\n\n private updateStats(): void {\n // Calculate FPS from frame intervals\n const avgFrameInterval = this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n \n // Calculate average seek time (frame update time)\n const avgSeekTime = this.seekTimes.length > 0\n ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length\n : 0;\n \n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom = avgSeekTime > 0\n ? this.TARGET_FRAME_TIME_MS - avgSeekTime\n : 0;\n \n // Get CPU pressure from adaptive tracker\n const trackerStats = this.adaptiveTracker.getStats();\n \n // Calculate displayed resolution from timegroup bounding rect\n const rect = this.timegroup.getBoundingClientRect();\n const renderWidth = Math.round(rect.width);\n const renderHeight = Math.round(rect.height);\n\n this.currentStats = {\n fps,\n avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,\n headroom: avgSeekTime > 0 ? headroom : null,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale: null, // Not applicable in DOM mode\n };\n }\n}\n\n/**\n * Factory function to create the appropriate stats tracking strategy for a presentation mode.\n * Returns null for modes that don't support stats tracking.\n */\nexport function createStatsTrackingStrategy(\n mode: PreviewPresentationMode,\n options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n canvasPreviewResult?: CanvasPreviewResult | null;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale?: () => number;\n isAtRest?: () => boolean;\n isExporting?: () => boolean;\n }\n): StatsTrackingStrategy | null {\n switch (mode) {\n case \"canvas\":\n if (!options.canvasPreviewResult || !options.getResolutionScale || !options.isAtRest || !options.isExporting) {\n return null;\n }\n return new CanvasStatsStrategy({\n canvasPreviewResult: options.canvasPreviewResult,\n adaptiveTracker: options.adaptiveTracker,\n compositionWidth: options.compositionWidth,\n compositionHeight: options.compositionHeight,\n getResolutionScale: options.getResolutionScale,\n isAtRest: options.isAtRest,\n isExporting: options.isExporting,\n });\n \n case \"dom\":\n case \"original\": // \"dom\" maps to \"original\" mode\n return new DomStatsStrategy({\n timegroup: options.timegroup,\n adaptiveTracker: options.adaptiveTracker,\n });\n \n case \"clone\":\n case \"computed\": // \"clone\" maps to \"computed\" mode\n // These modes don't support stats tracking\n return null;\n \n default:\n return null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
2
|
+
|
|
1
3
|
//#region src/preview/workers/WorkerPool.ts
|
|
2
|
-
/**
|
|
3
|
-
* Worker pool for parallel task execution.
|
|
4
|
-
* Manages a pool of workers and distributes tasks across them.
|
|
5
|
-
*/
|
|
6
|
-
const WORKER_TASK_TIMEOUT_MS = 3e4;
|
|
7
4
|
const WORKER_INIT_TEST_TIMEOUT_MS = 2e3;
|
|
8
5
|
var WorkerPool = class {
|
|
9
6
|
constructor(workerScriptUrl, poolSize = navigator.hardwareConcurrency || 4) {
|
|
@@ -46,7 +43,7 @@ var WorkerPool = class {
|
|
|
46
43
|
worker.addEventListener("message", testHandler);
|
|
47
44
|
worker.onerror = (error) => {
|
|
48
45
|
cleanupTest();
|
|
49
|
-
|
|
46
|
+
logger.error(`[WorkerPool] Worker ${i} error:`, {
|
|
50
47
|
message: error.message,
|
|
51
48
|
filename: error.filename,
|
|
52
49
|
lineno: error.lineno,
|
|
@@ -54,16 +51,16 @@ var WorkerPool = class {
|
|
|
54
51
|
});
|
|
55
52
|
};
|
|
56
53
|
worker.onmessageerror = (error) => {
|
|
57
|
-
|
|
54
|
+
logger.error(`[WorkerPool] Worker ${i} message error:`, error);
|
|
58
55
|
};
|
|
59
56
|
this.workers.push(worker);
|
|
60
57
|
this.availableWorkers.push(worker);
|
|
61
58
|
} catch (error) {
|
|
62
|
-
|
|
59
|
+
logger.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));
|
|
63
60
|
}
|
|
64
61
|
if (this.workers.length === 0) {
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
logger.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);
|
|
63
|
+
logger.error(`[WorkerPool] Browser support check:`, {
|
|
67
64
|
Worker: typeof Worker !== "undefined",
|
|
68
65
|
OffscreenCanvas: typeof OffscreenCanvas !== "undefined",
|
|
69
66
|
createImageBitmap: typeof createImageBitmap !== "undefined"
|
|
@@ -126,53 +123,7 @@ var WorkerPool = class {
|
|
|
126
123
|
this.availableWorkers = [];
|
|
127
124
|
}
|
|
128
125
|
};
|
|
129
|
-
/**
|
|
130
|
-
* Helper function to encode a canvas using a worker.
|
|
131
|
-
*/
|
|
132
|
-
async function encodeCanvasInWorker(worker, canvas, preserveAlpha) {
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;
|
|
135
|
-
performance.now();
|
|
136
|
-
let timeoutId = null;
|
|
137
|
-
const cleanup = () => {
|
|
138
|
-
if (timeoutId !== null) {
|
|
139
|
-
clearTimeout(timeoutId);
|
|
140
|
-
timeoutId = null;
|
|
141
|
-
}
|
|
142
|
-
worker.removeEventListener("message", messageHandler);
|
|
143
|
-
worker.removeEventListener("messageerror", messageErrorHandler);
|
|
144
|
-
};
|
|
145
|
-
const messageHandler = (event) => {
|
|
146
|
-
const result = event.data;
|
|
147
|
-
if (result.taskId === taskId) {
|
|
148
|
-
cleanup();
|
|
149
|
-
if (result.error) reject(/* @__PURE__ */ new Error(`Worker encoding failed: ${result.error}`));
|
|
150
|
-
else resolve(result.dataUrl);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
const messageErrorHandler = () => {
|
|
154
|
-
cleanup();
|
|
155
|
-
reject(/* @__PURE__ */ new Error("Worker message error"));
|
|
156
|
-
};
|
|
157
|
-
worker.addEventListener("message", messageHandler);
|
|
158
|
-
worker.addEventListener("messageerror", messageErrorHandler);
|
|
159
|
-
timeoutId = window.setTimeout(() => {
|
|
160
|
-
cleanup();
|
|
161
|
-
reject(/* @__PURE__ */ new Error("Worker task timed out"));
|
|
162
|
-
}, WORKER_TASK_TIMEOUT_MS);
|
|
163
|
-
createImageBitmap(canvas).then((bitmap) => {
|
|
164
|
-
worker.postMessage({
|
|
165
|
-
taskId,
|
|
166
|
-
bitmap,
|
|
167
|
-
preserveAlpha
|
|
168
|
-
}, [bitmap]);
|
|
169
|
-
}).catch((error) => {
|
|
170
|
-
cleanup();
|
|
171
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
126
|
|
|
176
127
|
//#endregion
|
|
177
|
-
export { WorkerPool
|
|
128
|
+
export { WorkerPool };
|
|
178
129
|
//# sourceMappingURL=WorkerPool.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null","timeoutId: number | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\n// Constants\nconst WORKER_TASK_TIMEOUT_MS = 30000;\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n private taskIdCounter = 0;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Create worker from URL (typically a blob URL from inlined worker code)\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n \n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n \n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n \n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n \n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (event.data && typeof event.data === \"string\" && event.data.includes(\"encoderWorker\")) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n \n worker.onerror = (error) => {\n cleanupTest();\n console.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n console.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n console.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));\n }\n }\n if (this.workers.length === 0) {\n console.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n console.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() &&\n this.workers.length > 0 &&\n !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({ \n resolve: resolve as (value: unknown) => void, \n reject, \n task \n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n \n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n\n/**\n * Helper function to encode a canvas using a worker.\n */\nexport async function encodeCanvasInWorker(\n worker: Worker,\n canvas: HTMLCanvasElement,\n preserveAlpha: boolean,\n): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const taskId = `task-${Date.now()}-${Math.random()}-${performance.now()}`;\n const startTime = performance.now();\n let timeoutId: number | null = null;\n\n const cleanup = () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n worker.removeEventListener(\"message\", messageHandler);\n worker.removeEventListener(\"messageerror\", messageErrorHandler);\n };\n\n const messageHandler = (event: MessageEvent) => {\n const result = event.data as { taskId: string; dataUrl: string; error?: string };\n if (result.taskId === taskId) {\n cleanup();\n if (result.error) {\n reject(new Error(`Worker encoding failed: ${result.error}`));\n } else {\n resolve(result.dataUrl);\n }\n }\n };\n\n const messageErrorHandler = () => {\n cleanup();\n reject(new Error(\"Worker message error\"));\n };\n\n worker.addEventListener(\"message\", messageHandler);\n worker.addEventListener(\"messageerror\", messageErrorHandler);\n\n // Set timeout to detect if worker never responds\n timeoutId = window.setTimeout(() => {\n cleanup();\n reject(new Error(\"Worker task timed out\"));\n }, WORKER_TASK_TIMEOUT_MS);\n\n // Create ImageBitmap from canvas\n createImageBitmap(canvas)\n .then((bitmap) => {\n // Transfer bitmap to worker (zero-copy)\n worker.postMessage(\n {\n taskId,\n bitmap,\n preserveAlpha,\n },\n [bitmap],\n );\n })\n .catch((error) => {\n cleanup();\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\nimport { logger } from \"../logger.js\";\n\n// Constants\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n private taskIdCounter = 0;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Create worker from URL (typically a blob URL from inlined worker code)\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n \n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n \n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n \n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n \n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (event.data && typeof event.data === \"string\" && event.data.includes(\"encoderWorker\")) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n \n worker.onerror = (error) => {\n cleanupTest();\n logger.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n logger.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n logger.error(`[WorkerPool] Failed to create worker ${i}:`, error instanceof Error ? error.message : String(error));\n }\n }\n if (this.workers.length === 0) {\n logger.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n logger.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() &&\n this.workers.length > 0 &&\n !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({ \n resolve: resolve as (value: unknown) => void, \n reject, \n task \n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n \n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RenderProgress, RenderToVideoOptions } from "../preview/renderTimegroupToVideo.js";
|
|
2
|
+
|
|
3
|
+
//#region src/render/EFRenderAPI.d.ts
|
|
4
|
+
|
|
5
|
+
interface IEFRenderAPI {
|
|
6
|
+
/**
|
|
7
|
+
* Render with streaming output (calls window.onRenderChunk for each chunk).
|
|
8
|
+
* Use this for CLI/Playwright to avoid memory buffering.
|
|
9
|
+
*/
|
|
10
|
+
renderStreaming(options?: RenderToVideoOptions): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Render and return buffer (for shorter videos or in-browser use).
|
|
13
|
+
* Returns the video as Uint8Array.
|
|
14
|
+
*/
|
|
15
|
+
render(options?: RenderToVideoOptions): Promise<Uint8Array>;
|
|
16
|
+
/**
|
|
17
|
+
* Get render info (dimensions, duration, assets).
|
|
18
|
+
* Same as the exported getRenderInfo function.
|
|
19
|
+
*/
|
|
20
|
+
getRenderInfo(): Promise<RenderInfo>;
|
|
21
|
+
/**
|
|
22
|
+
* Check if SDK is ready for rendering.
|
|
23
|
+
* Returns true if a root timegroup is found.
|
|
24
|
+
*/
|
|
25
|
+
isReady(): boolean;
|
|
26
|
+
}
|
|
27
|
+
declare global {
|
|
28
|
+
interface Window {
|
|
29
|
+
EF_RENDER?: IEFRenderAPI;
|
|
30
|
+
EF_RENDER_DATA?: Record<string, unknown>;
|
|
31
|
+
onRenderChunk?: (chunk: Uint8Array) => void;
|
|
32
|
+
onRenderProgress?: (progress: RenderProgress) => void;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=EFRenderAPI.d.ts.map
|
|
@@ -23,6 +23,7 @@ const api = {
|
|
|
23
23
|
try {
|
|
24
24
|
await timegroup.waitForMediaDurations();
|
|
25
25
|
const chunkWriter = new WritableStream({ write(chunk) {
|
|
26
|
+
console.error("Writing chunk", chunk);
|
|
26
27
|
if (window.onRenderChunk) window.onRenderChunk(chunk);
|
|
27
28
|
} });
|
|
28
29
|
const onProgress = options.onProgress || window.onRenderProgress;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFRenderAPI.js","names":["api: IEFRenderAPI"],"sources":["../../src/render/EFRenderAPI.ts"],"sourcesContent":["/**\n * Window API for programmatic video rendering.\n * \n * Exposes renderTimegroupToVideo for use from Playwright/CLI.\n * Supports streaming output and custom data injection.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { EFWorkbench } from \"../gui/EFWorkbench.js\";\nimport { getRenderInfo, type RenderInfo } from \"../getRenderInfo.js\";\nimport {\n renderTimegroupToVideo,\n type RenderToVideoOptions,\n type RenderProgress,\n} from \"../preview/renderTimegroupToVideo.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IEFRenderAPI {\n /**\n * Render with streaming output (calls window.onRenderChunk for each chunk).\n * Use this for CLI/Playwright to avoid memory buffering.\n */\n renderStreaming(options?: RenderToVideoOptions): Promise<void>;\n\n /**\n * Render and return buffer (for shorter videos or in-browser use).\n * Returns the video as Uint8Array.\n */\n render(options?: RenderToVideoOptions): Promise<Uint8Array>;\n\n /**\n * Get render info (dimensions, duration, assets).\n * Same as the exported getRenderInfo function.\n */\n getRenderInfo(): Promise<RenderInfo>;\n\n /**\n * Check if SDK is ready for rendering.\n * Returns true if a root timegroup is found.\n */\n isReady(): boolean;\n}\n\ndeclare global {\n interface Window {\n EF_RENDER?: IEFRenderAPI;\n EF_RENDER_DATA?: Record<string, unknown>;\n onRenderChunk?: (chunk: Uint8Array) => void; // Set by Playwright\n onRenderProgress?: (progress: RenderProgress) => void; // Optional progress callback\n }\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction findRootTimegroup(): EFTimegroup | null {\n // Try to find timegroup from workbench first\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n const timegroup = workbench.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n if (timegroup) {\n return timegroup;\n }\n }\n\n // Fallback: find first root timegroup\n const rootTimegroup = document.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n return rootTimegroup;\n}\n\nfunction setWorkbenchRendering(rendering: boolean): void {\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n workbench.rendering = rendering;\n }\n}\n\nconst api: IEFRenderAPI = {\n async renderStreaming(options: RenderToVideoOptions = {}): Promise<void> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Check if window.onRenderChunk is available\n if (typeof window === \"undefined\" || !window.onRenderChunk) {\n throw new Error(\n \"window.onRenderChunk is not set. \" +\n \"Call page.exposeFunction('onRenderChunk', callback) from Playwright first.\"\n );\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Create custom writable stream that calls window.onRenderChunk\n const chunkWriter = new WritableStream<Uint8Array>({\n write(chunk: Uint8Array) {\n if (window.onRenderChunk) {\n window.onRenderChunk(chunk);\n }\n },\n });\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Render with custom stream\n await renderTimegroupToVideo(timegroup, {\n ...options,\n customWritableStream: chunkWriter,\n onProgress,\n returnBuffer: false,\n });\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async render(options: RenderToVideoOptions = {}): Promise<Uint8Array> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n const buffer = await renderTimegroupToVideo(timegroup, {\n ...options,\n returnBuffer: true,\n onProgress,\n });\n\n if (!buffer) {\n throw new Error(\"Render failed: no buffer returned\");\n }\n\n return buffer;\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async getRenderInfo(): Promise<RenderInfo> {\n return getRenderInfo();\n },\n\n isReady(): boolean {\n return findRootTimegroup() !== null;\n },\n};\n\n// Export and register on window\nif (typeof window !== \"undefined\") {\n window.EF_RENDER = api;\n}\n\nexport { api as EFRenderAPI };\nexport type { IEFRenderAPI as EFRenderAPIInterface };\n"],"mappings":";;;;AA2DA,SAAS,oBAAwC;CAE/C,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,WAAW;EACb,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,UACF,QAAO;;AAMX,QADsB,SAAS,cAAc,eAAe;;AAI9D,SAAS,sBAAsB,WAA0B;CACvD,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,UACF,WAAU,YAAY;;AAI1B,MAAMA,MAAoB;CACxB,MAAM,gBAAgB,UAAgC,EAAE,EAAiB;EACvE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,cAC3C,OAAM,IAAI,MACR,8GAED;AAIH,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,UAAU,uBAAuB;GAGvC,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,OAAmB;AACvB,QAAI,OAAO,cACT,QAAO,cAAc,MAAM;MAGhC,CAAC;GAGF,MAAM,aAAa,QAAQ,cAAc,OAAO;AAGhD,SAAM,uBAAuB,WAAW;IACtC,GAAG;IACH,sBAAsB;IACtB;IACA,cAAc;IACf,CAAC;YACM;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,OAAO,UAAgC,EAAE,EAAuB;EACpE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,UAAU,uBAAuB;GAGvC,MAAM,aAAa,QAAQ,cAAc,OAAO;GAEhD,MAAM,SAAS,MAAM,uBAAuB,WAAW;IACrD,GAAG;IACH,cAAc;IACd;IACD,CAAC;AAEF,OAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,UAAO;YACC;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,gBAAqC;AACzC,SAAO,eAAe;;CAGxB,UAAmB;AACjB,SAAO,mBAAmB,KAAK;;CAElC;AAGD,IAAI,OAAO,WAAW,YACpB,QAAO,YAAY"}
|
|
1
|
+
{"version":3,"file":"EFRenderAPI.js","names":["api: IEFRenderAPI"],"sources":["../../src/render/EFRenderAPI.ts"],"sourcesContent":["/**\n * Window API for programmatic video rendering.\n * \n * Exposes renderTimegroupToVideo for use from Playwright/CLI.\n * Supports streaming output and custom data injection.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { EFWorkbench } from \"../gui/EFWorkbench.js\";\nimport { getRenderInfo, type RenderInfo } from \"../getRenderInfo.js\";\nimport {\n renderTimegroupToVideo,\n type RenderToVideoOptions,\n type RenderProgress,\n} from \"../preview/renderTimegroupToVideo.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IEFRenderAPI {\n /**\n * Render with streaming output (calls window.onRenderChunk for each chunk).\n * Use this for CLI/Playwright to avoid memory buffering.\n */\n renderStreaming(options?: RenderToVideoOptions): Promise<void>;\n\n /**\n * Render and return buffer (for shorter videos or in-browser use).\n * Returns the video as Uint8Array.\n */\n render(options?: RenderToVideoOptions): Promise<Uint8Array>;\n\n /**\n * Get render info (dimensions, duration, assets).\n * Same as the exported getRenderInfo function.\n */\n getRenderInfo(): Promise<RenderInfo>;\n\n /**\n * Check if SDK is ready for rendering.\n * Returns true if a root timegroup is found.\n */\n isReady(): boolean;\n}\n\ndeclare global {\n interface Window {\n EF_RENDER?: IEFRenderAPI;\n EF_RENDER_DATA?: Record<string, unknown>;\n onRenderChunk?: (chunk: Uint8Array) => void; // Set by Playwright\n onRenderProgress?: (progress: RenderProgress) => void; // Optional progress callback\n }\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction findRootTimegroup(): EFTimegroup | null {\n // Try to find timegroup from workbench first\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n const timegroup = workbench.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n if (timegroup) {\n return timegroup;\n }\n }\n\n // Fallback: find first root timegroup\n const rootTimegroup = document.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n return rootTimegroup;\n}\n\nfunction setWorkbenchRendering(rendering: boolean): void {\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n workbench.rendering = rendering;\n }\n}\n\nconst api: IEFRenderAPI = {\n async renderStreaming(options: RenderToVideoOptions = {}): Promise<void> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Check if window.onRenderChunk is available\n if (typeof window === \"undefined\" || !window.onRenderChunk) {\n throw new Error(\n \"window.onRenderChunk is not set. \" +\n \"Call page.exposeFunction('onRenderChunk', callback) from Playwright first.\"\n );\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Create custom writable stream that calls window.onRenderChunk\n const chunkWriter = new WritableStream<Uint8Array>({\n write(chunk: Uint8Array) {\n console.error(\"Writing chunk\", chunk);\n if (window.onRenderChunk) {\n window.onRenderChunk(chunk);\n }\n },\n });\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Render with custom stream\n await renderTimegroupToVideo(timegroup, {\n ...options,\n customWritableStream: chunkWriter,\n onProgress,\n returnBuffer: false,\n });\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async render(options: RenderToVideoOptions = {}): Promise<Uint8Array> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n const buffer = await renderTimegroupToVideo(timegroup, {\n ...options,\n returnBuffer: true,\n onProgress,\n });\n\n if (!buffer) {\n throw new Error(\"Render failed: no buffer returned\");\n }\n\n return buffer;\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async getRenderInfo(): Promise<RenderInfo> {\n return getRenderInfo();\n },\n\n isReady(): boolean {\n return findRootTimegroup() !== null;\n },\n};\n\n// Export and register on window\nif (typeof window !== \"undefined\") {\n window.EF_RENDER = api;\n}\n\nexport { api as EFRenderAPI };\nexport type { IEFRenderAPI as EFRenderAPIInterface };\n"],"mappings":";;;;AA2DA,SAAS,oBAAwC;CAE/C,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,WAAW;EACb,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,UACF,QAAO;;AAMX,QADsB,SAAS,cAAc,eAAe;;AAI9D,SAAS,sBAAsB,WAA0B;CACvD,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,UACF,WAAU,YAAY;;AAI1B,MAAMA,MAAoB;CACxB,MAAM,gBAAgB,UAAgC,EAAE,EAAiB;EACvE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,cAC3C,OAAM,IAAI,MACR,8GAED;AAIH,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,UAAU,uBAAuB;GAGvC,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,OAAmB;AACvB,YAAQ,MAAM,iBAAiB,MAAM;AACrC,QAAI,OAAO,cACT,QAAO,cAAc,MAAM;MAGhC,CAAC;GAGF,MAAM,aAAa,QAAQ,cAAc,OAAO;AAGhD,SAAM,uBAAuB,WAAW;IACtC,GAAG;IACH,sBAAsB;IACtB;IACA,cAAc;IACf,CAAC;YACM;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,OAAO,UAAgC,EAAE,EAAuB;EACpE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,UAAU,uBAAuB;GAGvC,MAAM,aAAa,QAAQ,cAAc,OAAO;GAEhD,MAAM,SAAS,MAAM,uBAAuB,WAAW;IACrD,GAAG;IACH,cAAc;IACd;IACD,CAAC;AAEF,OAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,UAAO;YACC;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,gBAAqC;AACzC,SAAO,eAAe;;CAGxB,UAAmB;AACjB,SAAO,mBAAmB,KAAK;;CAElC;AAGD,IAAI,OAAO,WAAW,YACpB,QAAO,YAAY"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../elements/EFTimegroup.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { TemplateResult } from "lit";
|
package/dist/style.css
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { MediaRendition } from "../../elements/EFMedia/shared/MediaTaskUtils.js";
|
|
2
|
-
|
|
3
1
|
//#region src/transcoding/types/index.d.ts
|
|
4
2
|
|
|
5
3
|
type RenditionId = "high" | "medium" | "low" | "audio" | "scrub";
|
|
@@ -19,6 +17,11 @@ interface VideoRendition {
|
|
|
19
17
|
segmentDurationsMs?: number[];
|
|
20
18
|
startTimeOffsetMs?: number;
|
|
21
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Union type representing either an audio or video rendition.
|
|
22
|
+
* Used in methods that can work with either type of media rendition.
|
|
23
|
+
*/
|
|
24
|
+
type MediaRendition = AudioRendition | VideoRendition;
|
|
22
25
|
interface MediaEngine {
|
|
23
26
|
durationMs: number;
|
|
24
27
|
src: string;
|
|
@@ -100,5 +103,5 @@ interface SegmentTimeRange {
|
|
|
100
103
|
endMs: number;
|
|
101
104
|
}
|
|
102
105
|
//#endregion
|
|
103
|
-
export {
|
|
106
|
+
export { AudioSpan, MediaEngine, RenditionId };
|
|
104
107
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.5-beta",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
"license": "UNLICENSED",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@bramus/style-observer": "^1.3.0",
|
|
16
|
-
"@editframe/assets": "0.
|
|
16
|
+
"@editframe/assets": "0.34.5-beta",
|
|
17
17
|
"@lit/context": "^1.1.6",
|
|
18
|
-
"@lit/task": "^1.0.3",
|
|
19
18
|
"@opentelemetry/api": "^1.9.0",
|
|
20
19
|
"@opentelemetry/context-zone": "^1.26.0",
|
|
21
20
|
"@opentelemetry/core": "^1.26.0",
|
|
@@ -6,7 +6,14 @@ import type { EFVideo } from "../src/elements/EFVideo.js";
|
|
|
6
6
|
import "../src/elements/EFVideo.js";
|
|
7
7
|
import { assetMSWHandlers } from "./useAssetMSW.js";
|
|
8
8
|
import "../src/elements/EFTimegroup.js";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// Status values match the old TaskStatus enum: INITIAL=0, PENDING=1, COMPLETE=2, ERROR=3
|
|
11
|
+
const TaskStatus = {
|
|
12
|
+
INITIAL: 0,
|
|
13
|
+
PENDING: 1,
|
|
14
|
+
COMPLETE: 2,
|
|
15
|
+
ERROR: 3,
|
|
16
|
+
};
|
|
10
17
|
|
|
11
18
|
const test = baseTest.extend({
|
|
12
19
|
setupAssetHandlers: [
|
package/test/profilingPlugin.ts
CHANGED
|
@@ -90,8 +90,6 @@ export function profilingPlugin(): Plugin {
|
|
|
90
90
|
* Call this from test setup or beforeAll
|
|
91
91
|
*/
|
|
92
92
|
export async function startProfiling(page: any): Promise<void> {
|
|
93
|
-
if (process.env.VITEST_PROFILE !== "1") return;
|
|
94
|
-
|
|
95
93
|
try {
|
|
96
94
|
// Get CDP session from Playwright page
|
|
97
95
|
// In Playwright, we access CDP via page.context().newCDPSession(page)
|
|
@@ -116,7 +114,7 @@ export async function startProfiling(page: any): Promise<void> {
|
|
|
116
114
|
export async function stopProfiling(
|
|
117
115
|
outputPath?: string,
|
|
118
116
|
): Promise<CPUProfile | null> {
|
|
119
|
-
if (
|
|
117
|
+
if (!cdpSession) return null;
|
|
120
118
|
|
|
121
119
|
try {
|
|
122
120
|
const { profile } = (await cdpSession.send("Profiler.stop")) as {
|
package/test/setup.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This runs before every test to ensure clean state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { beforeEach } from "vitest";
|
|
6
|
+
import { beforeEach, afterAll } from "vitest";
|
|
7
7
|
import {
|
|
8
8
|
globalRequestDeduplicator,
|
|
9
9
|
mediaCache,
|
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
import { globalURLTokenDeduplicator } from "../src/transcoding/cache/URLTokenDeduplicator.js";
|
|
12
12
|
import { TEST_SERVER_PORT } from "./constants.js";
|
|
13
13
|
|
|
14
|
+
// Type declarations for test environment
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
__CI_MODE__?: boolean;
|
|
18
|
+
__PROFILER_STOP_REQUESTED__?: boolean;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Get the correct API host for the current environment.
|
|
16
24
|
* In local dev with Traefik, returns the Traefik URL (e.g., http://main.localhost:4322).
|
|
@@ -48,3 +56,17 @@ beforeEach(() => {
|
|
|
48
56
|
mediaCache.clear();
|
|
49
57
|
globalURLTokenDeduplicator.clear();
|
|
50
58
|
});
|
|
59
|
+
|
|
60
|
+
// Signal profiler to stop before tests finish
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
if (typeof window !== "undefined") {
|
|
63
|
+
// Always set the flag, creating it if it doesn't exist
|
|
64
|
+
// This handles both profiled and non-profiled test runs
|
|
65
|
+
console.log("[Profiler] Signaling stop...");
|
|
66
|
+
(window as any).__PROFILER_STOP_REQUESTED__ = true;
|
|
67
|
+
|
|
68
|
+
// Give profiler time to detect the signal and retrieve profile data
|
|
69
|
+
// Poll interval is 50ms, so wait longer to ensure detection + retrieval
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, 500));
|
|
71
|
+
}
|
|
72
|
+
});
|
package/dist/EF_INTERACTIVE.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EF_INTERACTIVE.js","names":[],"sources":["../src/EF_INTERACTIVE.ts"],"sourcesContent":["export let EF_INTERACTIVE = false;\n\nif (typeof window !== \"undefined\") {\n EF_INTERACTIVE = !window.location?.search.includes(\"EF_NONINTERACTIVE\");\n}\n\n/**\n * Set EF_INTERACTIVE value for testing purposes.\n * @internal\n */\nexport const setEFInteractive = (value: boolean) => {\n EF_INTERACTIVE = value;\n};\n"],"mappings":";AAAA,IAAW,iBAAiB;AAE5B,IAAI,OAAO,WAAW,YACpB,kBAAiB,CAAC,OAAO,UAAU,OAAO,SAAS,oBAAoB"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { MediaSample, SampleBuffer } from "../SampleBuffer.js";
|
|
2
|
-
import * as mediabunny3 from "mediabunny";
|
|
3
|
-
import { AudioSampleSink, InputAudioTrack, InputTrack, InputVideoTrack, VideoSampleSink } from "mediabunny";
|
|
4
|
-
|
|
5
|
-
//#region src/elements/EFMedia/BufferedSeekingInput.d.ts
|
|
6
|
-
interface BufferedSeekingInputOptions {
|
|
7
|
-
videoBufferSize?: number;
|
|
8
|
-
audioBufferSize?: number;
|
|
9
|
-
/**
|
|
10
|
-
* Timeline offset in milliseconds to map user timeline to media timeline.
|
|
11
|
-
* Applied during seeking to handle media that doesn't start at 0ms.
|
|
12
|
-
*/
|
|
13
|
-
startTimeOffsetMs?: number;
|
|
14
|
-
}
|
|
15
|
-
declare class BufferedSeekingInput {
|
|
16
|
-
#private;
|
|
17
|
-
private input;
|
|
18
|
-
private trackIterators;
|
|
19
|
-
private trackBuffers;
|
|
20
|
-
private options;
|
|
21
|
-
private trackIteratorCreationPromises;
|
|
22
|
-
private trackSeekPromises;
|
|
23
|
-
/**
|
|
24
|
-
* Timeline offset in milliseconds to map user timeline to media timeline.
|
|
25
|
-
* Applied during seeking to handle media that doesn't start at 0ms.
|
|
26
|
-
*/
|
|
27
|
-
private readonly startTimeOffsetMs;
|
|
28
|
-
constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions);
|
|
29
|
-
getBufferSize(trackId: number): number;
|
|
30
|
-
getBufferContents(trackId: number): readonly MediaSample[];
|
|
31
|
-
getBufferTimestamps(trackId: number): number[];
|
|
32
|
-
clearBuffer(trackId: number): void;
|
|
33
|
-
computeDuration(): Promise<number>;
|
|
34
|
-
getTrack(trackId: number): Promise<InputTrack>;
|
|
35
|
-
getAudioTrack(trackId: number): Promise<InputAudioTrack>;
|
|
36
|
-
getVideoTrack(trackId: number): Promise<InputVideoTrack>;
|
|
37
|
-
getFirstVideoTrack(): Promise<InputVideoTrack | undefined>;
|
|
38
|
-
getFirstAudioTrack(): Promise<InputAudioTrack | undefined>;
|
|
39
|
-
getTrackIterator(track: InputTrack): AsyncIterator<MediaSample, any, any>;
|
|
40
|
-
createTrackSampleSink(track: InputTrack): AudioSampleSink | VideoSampleSink;
|
|
41
|
-
createTrackIterator(track: InputTrack): AsyncGenerator<mediabunny3.VideoSample, void, unknown> | AsyncGenerator<mediabunny3.AudioSample, void, unknown>;
|
|
42
|
-
createTrackBuffer(track: InputTrack): SampleBuffer;
|
|
43
|
-
getTrackBuffer(track: InputTrack): SampleBuffer;
|
|
44
|
-
seek(trackId: number, timeMs: number): Promise<MediaSample | undefined>;
|
|
45
|
-
private resetIterator;
|
|
46
|
-
private seekSafe;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
export { BufferedSeekingInput };
|
|
50
|
-
//# sourceMappingURL=BufferedSeekingInput.d.ts.map
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { MediaBufferState } from "../shared/BufferUtils.js";
|
|
2
|
-
import { Task } from "@lit/task";
|
|
3
|
-
|
|
4
|
-
//#region src/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* State of the audio buffer - uses the generic interface
|
|
8
|
-
*/
|
|
9
|
-
interface AudioBufferState extends MediaBufferState {}
|
|
10
|
-
//#endregion
|
|
11
|
-
export { AudioBufferState };
|
|
12
|
-
//# sourceMappingURL=makeAudioBufferTask.d.ts.map
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
2
|
-
import { EF_RENDERING } from "../../../EF_RENDERING.js";
|
|
3
|
-
import { AssetMediaEngine } from "../AssetMediaEngine.js";
|
|
4
|
-
import { manageMediaBuffer } from "../shared/BufferUtils.js";
|
|
5
|
-
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
|
|
6
|
-
import { Task } from "@lit/task";
|
|
7
|
-
|
|
8
|
-
//#region src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts
|
|
9
|
-
const makeAudioBufferTask = (host) => {
|
|
10
|
-
let currentState = {
|
|
11
|
-
currentSeekTimeMs: 0,
|
|
12
|
-
requestedSegments: /* @__PURE__ */ new Set(),
|
|
13
|
-
activeRequests: /* @__PURE__ */ new Set(),
|
|
14
|
-
requestQueue: []
|
|
15
|
-
};
|
|
16
|
-
let task;
|
|
17
|
-
task = new Task(host, {
|
|
18
|
-
autoRun: EF_INTERACTIVE,
|
|
19
|
-
args: () => [host.desiredSeekTimeMs],
|
|
20
|
-
onError: (error) => {
|
|
21
|
-
task.taskComplete.catch(() => {});
|
|
22
|
-
if (error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message?.includes("signal is aborted") || error.message?.includes("The user aborted a request"))) return;
|
|
23
|
-
if (error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("is not valid JSON") || error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch"))) return;
|
|
24
|
-
console.error("audioBufferTask error", error);
|
|
25
|
-
},
|
|
26
|
-
onComplete: (value) => {
|
|
27
|
-
currentState = value;
|
|
28
|
-
},
|
|
29
|
-
task: async ([seekTimeMs], { signal }) => {
|
|
30
|
-
if (EF_RENDERING()) return currentState;
|
|
31
|
-
if (host.mediaEngineTask.error) return currentState;
|
|
32
|
-
let mediaEngine;
|
|
33
|
-
try {
|
|
34
|
-
mediaEngine = await getLatestMediaEngine(host, signal);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error instanceof Error && error.message === "No valid media source") return currentState;
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
if (!mediaEngine) return currentState;
|
|
40
|
-
if (!mediaEngine.audioRendition) return currentState;
|
|
41
|
-
const engineConfig = mediaEngine.getBufferConfig();
|
|
42
|
-
const currentConfig = {
|
|
43
|
-
bufferDurationMs: engineConfig.audioBufferDurationMs,
|
|
44
|
-
maxParallelFetches: engineConfig.maxAudioBufferFetches,
|
|
45
|
-
enableBuffering: host.enableAudioBuffering,
|
|
46
|
-
bufferThresholdMs: engineConfig.bufferThresholdMs
|
|
47
|
-
};
|
|
48
|
-
const timelineContext = host.rootTimegroup?.currentTimeMs !== void 0 ? {
|
|
49
|
-
elementStartMs: host.startTimeMs,
|
|
50
|
-
elementEndMs: host.endTimeMs,
|
|
51
|
-
playheadMs: host.rootTimegroup.currentTimeMs
|
|
52
|
-
} : void 0;
|
|
53
|
-
return manageMediaBuffer(seekTimeMs, currentConfig, currentState, host.intrinsicDurationMs || 1e4, signal, {
|
|
54
|
-
computeSegmentId: async (timeMs, rendition) => {
|
|
55
|
-
try {
|
|
56
|
-
const mediaEngine$1 = await getLatestMediaEngine(host, signal);
|
|
57
|
-
if (!mediaEngine$1) return void 0;
|
|
58
|
-
return mediaEngine$1.computeSegmentId(timeMs, rendition);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (error instanceof Error && error.message === "No valid media source") return;
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
prefetchSegment: async (segmentId, rendition) => {
|
|
65
|
-
try {
|
|
66
|
-
const mediaEngine$1 = await getLatestMediaEngine(host, signal);
|
|
67
|
-
if (!mediaEngine$1) return;
|
|
68
|
-
if (mediaEngine$1 instanceof AssetMediaEngine) {
|
|
69
|
-
const trackData = mediaEngine$1.data?.[rendition.trackId];
|
|
70
|
-
if (!trackData?.segments || segmentId >= trackData.segments.length) return;
|
|
71
|
-
}
|
|
72
|
-
await mediaEngine$1.fetchMediaSegment(segmentId, rendition, signal);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (error instanceof Error && (error.message === "No valid media source" || error.message.includes("Media segment not found") || error.message.includes("Track not found") || error.message.includes("Failed to fetch") || error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("File not found"))) return;
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
isSegmentCached: (segmentId, rendition) => {
|
|
79
|
-
const mediaEngine$1 = host.mediaEngineTask.value;
|
|
80
|
-
if (!mediaEngine$1) return false;
|
|
81
|
-
return mediaEngine$1.isSegmentCached(segmentId, rendition);
|
|
82
|
-
},
|
|
83
|
-
getRendition: async () => {
|
|
84
|
-
try {
|
|
85
|
-
const mediaEngine$1 = await getLatestMediaEngine(host, signal);
|
|
86
|
-
if (!mediaEngine$1) throw new Error("Audio rendition not available");
|
|
87
|
-
const audioRendition = mediaEngine$1.audioRendition;
|
|
88
|
-
if (!audioRendition) throw new Error("Audio rendition not available");
|
|
89
|
-
return audioRendition;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (error instanceof Error && error.message === "No valid media source") throw new Error("Audio rendition not available");
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
logError: console.error
|
|
96
|
-
}, timelineContext);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
return task;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
//#endregion
|
|
103
|
-
export { makeAudioBufferTask };
|
|
104
|
-
//# sourceMappingURL=makeAudioBufferTask.js.map
|