@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,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as lit25 from "lit";
|
|
2
2
|
import { LitElement } from "lit";
|
|
3
|
-
import * as
|
|
3
|
+
import * as lit_html24 from "lit-html";
|
|
4
4
|
|
|
5
5
|
//#region src/gui/EFResizableBox.d.ts
|
|
6
6
|
interface BoxBounds {
|
|
@@ -21,7 +21,7 @@ declare class EFResizableBox extends LitElement {
|
|
|
21
21
|
private resizeStartCorner;
|
|
22
22
|
private resizeStartSize;
|
|
23
23
|
private resizeStartPosition;
|
|
24
|
-
static styles:
|
|
24
|
+
static styles: lit25.CSSResult;
|
|
25
25
|
private resizeObserver?;
|
|
26
26
|
connectedCallback(): void;
|
|
27
27
|
disconnectedCallback(): void;
|
|
@@ -30,7 +30,7 @@ declare class EFResizableBox extends LitElement {
|
|
|
30
30
|
private handlePointerUp;
|
|
31
31
|
private cleanup;
|
|
32
32
|
private dispatchBoundsChange;
|
|
33
|
-
render():
|
|
33
|
+
render(): lit_html24.TemplateResult<1>;
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
36
|
export { BoxBounds, EFResizableBox };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
2
2
|
import { getCornerPoint, getOppositeCorner } from "./transformUtils.js";
|
|
3
3
|
import { LitElement, css, html } from "lit";
|
|
4
4
|
import { customElement, property, state } from "lit/decorators.js";
|
package/dist/gui/EFScrubber.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit18 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html18 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFScrubber.d.ts
|
|
7
7
|
declare const EFScrubber_base: (new (...args: any[]) => {
|
|
@@ -10,7 +10,7 @@ declare const EFScrubber_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFScrubber extends EFScrubber_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit18.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
contextCurrentTimeMs: number;
|
|
16
16
|
contextDurationMs: number;
|
|
@@ -50,7 +50,7 @@ declare class EFScrubber extends EFScrubber_base {
|
|
|
50
50
|
private boundHandlePointerUp;
|
|
51
51
|
private boundHandlePointerCancel;
|
|
52
52
|
private boundHandleContextMenu;
|
|
53
|
-
render():
|
|
53
|
+
render(): lit_html18.TemplateResult<1>;
|
|
54
54
|
connectedCallback(): void;
|
|
55
55
|
disconnectedCallback(): void;
|
|
56
56
|
}
|
package/dist/gui/EFScrubber.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { currentTimeContext } from "./currentTimeContext.js";
|
|
2
2
|
import { durationContext } from "./durationContext.js";
|
|
3
3
|
import { playingContext } from "./playingContext.js";
|
|
4
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
5
5
|
import { efContext } from "./efContext.js";
|
|
6
6
|
import { quantizeToFrameTimeMs } from "./EFTimelineRuler.js";
|
|
7
7
|
import { TargetOrContextMixin } from "./TargetOrContextMixin.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit19 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html19 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTimeDisplay.d.ts
|
|
7
7
|
declare const EFTimeDisplay_base: (new (...args: any[]) => {
|
|
@@ -10,11 +10,11 @@ declare const EFTimeDisplay_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFTimeDisplay extends EFTimeDisplay_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit19.CSSResult;
|
|
14
14
|
currentTimeMs: number;
|
|
15
15
|
durationMs: number;
|
|
16
16
|
private formatTime;
|
|
17
|
-
render():
|
|
17
|
+
render(): lit_html19.TemplateResult<1>;
|
|
18
18
|
}
|
|
19
19
|
declare global {
|
|
20
20
|
interface HTMLElementTagNameMap {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { currentTimeContext } from "./currentTimeContext.js";
|
|
2
2
|
import { durationContext } from "./durationContext.js";
|
|
3
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
4
4
|
import { efContext } from "./efContext.js";
|
|
5
5
|
import { TargetOrContextMixin } from "./TargetOrContextMixin.js";
|
|
6
6
|
import { consume } from "@lit/context";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TimelineState } from "./timeline/timelineStateContext.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit33 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html32 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTimelineRuler.d.ts
|
|
7
7
|
/**
|
|
@@ -25,7 +25,7 @@ declare function calculatePixelsPerFrame(frameIntervalMs: number, pixelsPerMs: n
|
|
|
25
25
|
*/
|
|
26
26
|
declare function shouldShowFrameMarkers(pixelsPerFrame: number, minSpacing?: number): boolean;
|
|
27
27
|
declare class EFTimelineRuler extends LitElement {
|
|
28
|
-
static styles:
|
|
28
|
+
static styles: lit33.CSSResult[];
|
|
29
29
|
durationMs: number;
|
|
30
30
|
contextDurationMs: number;
|
|
31
31
|
timelineState?: TimelineState;
|
|
@@ -59,7 +59,7 @@ declare class EFTimelineRuler extends LitElement {
|
|
|
59
59
|
private calculateLabelInterval;
|
|
60
60
|
private getVisibleLabels;
|
|
61
61
|
private renderCanvas;
|
|
62
|
-
render():
|
|
62
|
+
render(): lit_html32.TemplateResult<1>;
|
|
63
63
|
}
|
|
64
64
|
declare global {
|
|
65
65
|
interface HTMLElementTagNameMap {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { durationContext } from "./durationContext.js";
|
|
2
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
3
3
|
import { DEFAULT_PIXELS_PER_MS, timelineStateContext } from "./timeline/timelineStateContext.js";
|
|
4
4
|
import { consume } from "@lit/context";
|
|
5
5
|
import { LitElement, css, html } from "lit";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit17 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html17 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFToggleLoop.d.ts
|
|
7
7
|
declare const EFToggleLoop_base: (new (...args: any[]) => {
|
|
@@ -10,9 +10,9 @@ declare const EFToggleLoop_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFToggleLoop extends EFToggleLoop_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit17.CSSResult[];
|
|
14
14
|
get context(): ControllableInterface | null;
|
|
15
|
-
render():
|
|
15
|
+
render(): lit_html17.TemplateResult<1>;
|
|
16
16
|
}
|
|
17
17
|
declare global {
|
|
18
18
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFToggleLoop.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
2
2
|
import { efContext } from "./efContext.js";
|
|
3
3
|
import { TargetOrContextMixin } from "./TargetOrContextMixin.js";
|
|
4
4
|
import { LitElement, css, html } from "lit";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ControllableInterface } from "./Controllable.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit14 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html14 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTogglePlay.d.ts
|
|
7
7
|
declare const EFTogglePlay_base: (new (...args: any[]) => {
|
|
@@ -10,12 +10,12 @@ declare const EFTogglePlay_base: (new (...args: any[]) => {
|
|
|
10
10
|
effectiveContext: ControllableInterface | null;
|
|
11
11
|
}) & typeof LitElement;
|
|
12
12
|
declare class EFTogglePlay extends EFTogglePlay_base {
|
|
13
|
-
static styles:
|
|
13
|
+
static styles: lit14.CSSResult[];
|
|
14
14
|
playing: boolean;
|
|
15
15
|
get efContext(): ControllableInterface | null;
|
|
16
16
|
connectedCallback(): void;
|
|
17
17
|
disconnectedCallback(): void;
|
|
18
|
-
render():
|
|
18
|
+
render(): lit_html14.TemplateResult<1>;
|
|
19
19
|
togglePlay: () => void;
|
|
20
20
|
private getPlaybackController;
|
|
21
21
|
}
|
package/dist/gui/EFTogglePlay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { playingContext } from "./playingContext.js";
|
|
2
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
3
3
|
import { isEFTemporal } from "../elements/EFTemporal.js";
|
|
4
4
|
import { efContext } from "./efContext.js";
|
|
5
5
|
import { attachContextRoot } from "../attachContextRoot.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PanZoomTransform } from "../elements/EFPanZoom.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as lit24 from "lit";
|
|
3
3
|
import { LitElement } from "lit";
|
|
4
|
-
import * as
|
|
4
|
+
import * as lit_html23 from "lit-html";
|
|
5
5
|
|
|
6
6
|
//#region src/gui/EFTransformHandles.d.ts
|
|
7
7
|
interface TransformBounds {
|
|
@@ -49,7 +49,7 @@ declare class EFTransformHandles extends LitElement {
|
|
|
49
49
|
* Note: Not a @state() property to avoid re-renders during interaction.
|
|
50
50
|
*/
|
|
51
51
|
private initialBounds;
|
|
52
|
-
static styles:
|
|
52
|
+
static styles: lit24.CSSResult;
|
|
53
53
|
private resizeObserver?;
|
|
54
54
|
/**
|
|
55
55
|
* Single source of truth for zoom scale.
|
|
@@ -79,7 +79,7 @@ declare class EFTransformHandles extends LitElement {
|
|
|
79
79
|
private handleMouseMove;
|
|
80
80
|
private handleMouseUp;
|
|
81
81
|
private cleanup;
|
|
82
|
-
render():
|
|
82
|
+
render(): lit_html23.TemplateResult<1>;
|
|
83
83
|
}
|
|
84
84
|
declare global {
|
|
85
85
|
interface HTMLElementTagNameMap {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
2
2
|
import { panZoomTransformContext } from "./panZoomTransformContext.js";
|
|
3
3
|
import { getCornerPoint, getOppositeCorner } from "./transformUtils.js";
|
|
4
4
|
import { calculateDragBounds, calculateResizeBounds, getResizeHandleCursor } from "./transformCalculations.js";
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ContextMixinInterface } from "./ContextMixin.js";
|
|
2
2
|
import { RenderToVideoOptions } from "../preview/renderTimegroupToVideo.js";
|
|
3
|
-
import
|
|
3
|
+
import "./EFFitScale.js";
|
|
4
|
+
import * as lit8 from "lit";
|
|
4
5
|
import { LitElement, PropertyValueMap } from "lit";
|
|
5
|
-
import * as
|
|
6
|
+
import * as lit_html8 from "lit-html";
|
|
6
7
|
import * as lit_html_directives_ref_js2 from "lit-html/directives/ref.js";
|
|
7
8
|
|
|
8
9
|
//#region src/gui/EFWorkbench.d.ts
|
|
9
10
|
declare const EFWorkbench_base: (new (...args: any[]) => ContextMixinInterface) & typeof LitElement;
|
|
10
11
|
declare class EFWorkbench extends EFWorkbench_base {
|
|
11
|
-
static styles:
|
|
12
|
+
static styles: lit8.CSSResult[];
|
|
12
13
|
rendering: boolean;
|
|
13
14
|
private panZoomTransform;
|
|
14
15
|
private isExporting;
|
|
@@ -193,7 +194,7 @@ declare class EFWorkbench extends EFWorkbench_base {
|
|
|
193
194
|
updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
|
|
194
195
|
drawOverlays: () => void;
|
|
195
196
|
private renderPlaybackStats;
|
|
196
|
-
render():
|
|
197
|
+
render(): lit_html8.TemplateResult<1>;
|
|
197
198
|
}
|
|
198
199
|
declare global {
|
|
199
200
|
interface HTMLElementTagNameMap {
|
package/dist/gui/EFWorkbench.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
2
2
|
import { ContextMixin } from "./ContextMixin.js";
|
|
3
3
|
import { TWMixin } from "./TWMixin2.js";
|
|
4
4
|
import { renderTimegroupPreview } from "../preview/renderTimegroupPreview.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderFrameOptions } from "../preview/FrameController.js";
|
|
2
2
|
import { ReactiveController, ReactiveControllerHost } from "lit";
|
|
3
3
|
|
|
4
4
|
//#region src/gui/PlaybackController.d.ts
|
|
@@ -10,6 +10,11 @@ interface PlaybackHost extends HTMLElement, ReactiveControllerHost {
|
|
|
10
10
|
run(): void;
|
|
11
11
|
taskComplete: Promise<unknown>;
|
|
12
12
|
};
|
|
13
|
+
/** New centralized frame controller (replaces frameTask) */
|
|
14
|
+
frameController?: {
|
|
15
|
+
renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<void>;
|
|
16
|
+
abort(): void;
|
|
17
|
+
};
|
|
13
18
|
renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;
|
|
14
19
|
waitForMediaDurations?(signal?: AbortSignal): Promise<void>;
|
|
15
20
|
saveTimeToLocalStorage?(time: number): void;
|
|
@@ -43,7 +48,6 @@ type PlaybackControllerUpdateEvent = {
|
|
|
43
48
|
*/
|
|
44
49
|
declare class PlaybackController implements ReactiveController {
|
|
45
50
|
#private;
|
|
46
|
-
seekTask: Task<readonly [number | undefined], number | undefined>;
|
|
47
51
|
constructor(host: PlaybackHost);
|
|
48
52
|
get currentTime(): number;
|
|
49
53
|
set currentTime(time: number);
|
|
@@ -59,6 +63,10 @@ declare class PlaybackController implements ReactiveController {
|
|
|
59
63
|
hostDisconnected(): void;
|
|
60
64
|
hostUpdated(): void;
|
|
61
65
|
static readonly THROTTLED_FRAME_TASK_MAX_WAITS = 100;
|
|
66
|
+
/**
|
|
67
|
+
* Run frame rendering with throttling to prevent concurrent executions.
|
|
68
|
+
* Uses the new FrameController when available, falling back to frameTask.
|
|
69
|
+
*/
|
|
62
70
|
runThrottledFrameTask(): Promise<void>;
|
|
63
71
|
addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void;
|
|
64
72
|
removeListener(listener: (event: PlaybackControllerUpdateEvent) => void): void;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
2
1
|
import { currentTimeContext } from "./currentTimeContext.js";
|
|
3
2
|
import { durationContext } from "./durationContext.js";
|
|
4
3
|
import { loopContext, playingContext } from "./playingContext.js";
|
|
4
|
+
import { updateAnimations } from "../elements/updateAnimations.js";
|
|
5
5
|
import { ContextProvider } from "@lit/context";
|
|
6
|
-
import { Task, TaskStatus } from "@lit/task";
|
|
7
6
|
|
|
8
7
|
//#region src/gui/PlaybackController.ts
|
|
9
8
|
/**
|
|
@@ -43,34 +42,10 @@ var PlaybackController = class PlaybackController {
|
|
|
43
42
|
#processingPendingSeek = false;
|
|
44
43
|
#loopingPlayback = false;
|
|
45
44
|
#playbackWrapTimeSeconds = 0;
|
|
45
|
+
#seekAbortController = null;
|
|
46
46
|
constructor(host) {
|
|
47
47
|
this.#host = host;
|
|
48
48
|
host.addController(this);
|
|
49
|
-
this.seekTask = new Task(this.#host, {
|
|
50
|
-
autoRun: false,
|
|
51
|
-
args: () => [this.#pendingSeekTime ?? this.#currentTime],
|
|
52
|
-
onComplete: () => {},
|
|
53
|
-
task: async ([targetTime], { signal }) => {
|
|
54
|
-
signal?.throwIfAborted();
|
|
55
|
-
await this.#host.waitForMediaDurations?.(signal);
|
|
56
|
-
signal?.throwIfAborted();
|
|
57
|
-
const newTime = Math.max(0, Math.min(targetTime ?? 0, this.#host.durationMs / 1e3));
|
|
58
|
-
this.#currentTime = newTime;
|
|
59
|
-
this.#host.requestUpdate("currentTime");
|
|
60
|
-
this.#currentTimeMsProvider.setValue(this.currentTimeMs);
|
|
61
|
-
this.#notifyListeners({
|
|
62
|
-
property: "currentTimeMs",
|
|
63
|
-
value: this.currentTimeMs
|
|
64
|
-
});
|
|
65
|
-
signal?.throwIfAborted();
|
|
66
|
-
await this.runThrottledFrameTask();
|
|
67
|
-
signal?.throwIfAborted();
|
|
68
|
-
if (!(this.#host.isRestoringFromLocalStorage?.() ?? false)) this.#host.saveTimeToLocalStorage?.(newTime);
|
|
69
|
-
else this.#host.setRestoringFromLocalStorage?.(false);
|
|
70
|
-
this.#seekInProgress = false;
|
|
71
|
-
return newTime;
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
49
|
this.#playingProvider = new ContextProvider(host, {
|
|
75
50
|
context: playingContext,
|
|
76
51
|
initialValue: this.#playing
|
|
@@ -109,7 +84,7 @@ var PlaybackController = class PlaybackController {
|
|
|
109
84
|
}
|
|
110
85
|
this.#currentTime = time;
|
|
111
86
|
this.#seekInProgress = true;
|
|
112
|
-
this
|
|
87
|
+
this.#runSeek(time).finally(() => {
|
|
113
88
|
if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
|
|
114
89
|
const pendingTime = this.#pendingSeekTime;
|
|
115
90
|
this.#pendingSeekTime = void 0;
|
|
@@ -122,6 +97,34 @@ var PlaybackController = class PlaybackController {
|
|
|
122
97
|
} else this.#pendingSeekTime = void 0;
|
|
123
98
|
});
|
|
124
99
|
}
|
|
100
|
+
async #runSeek(targetTime) {
|
|
101
|
+
this.#seekAbortController?.abort();
|
|
102
|
+
this.#seekAbortController = new AbortController();
|
|
103
|
+
const signal = this.#seekAbortController.signal;
|
|
104
|
+
try {
|
|
105
|
+
signal.throwIfAborted();
|
|
106
|
+
await this.#host.waitForMediaDurations?.(signal);
|
|
107
|
+
signal.throwIfAborted();
|
|
108
|
+
const newTime = Math.max(0, Math.min(targetTime, this.#host.durationMs / 1e3));
|
|
109
|
+
this.#currentTime = newTime;
|
|
110
|
+
this.#host.requestUpdate("currentTime");
|
|
111
|
+
this.#currentTimeMsProvider.setValue(this.currentTimeMs);
|
|
112
|
+
this.#notifyListeners({
|
|
113
|
+
property: "currentTimeMs",
|
|
114
|
+
value: this.currentTimeMs
|
|
115
|
+
});
|
|
116
|
+
signal.throwIfAborted();
|
|
117
|
+
await this.runThrottledFrameTask();
|
|
118
|
+
signal.throwIfAborted();
|
|
119
|
+
if (!(this.#host.isRestoringFromLocalStorage?.() ?? false)) this.#host.saveTimeToLocalStorage?.(newTime);
|
|
120
|
+
else this.#host.setRestoringFromLocalStorage?.(false);
|
|
121
|
+
this.#seekInProgress = false;
|
|
122
|
+
return newTime;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
125
128
|
get playing() {
|
|
126
129
|
return this.#playing;
|
|
127
130
|
}
|
|
@@ -189,7 +192,6 @@ var PlaybackController = class PlaybackController {
|
|
|
189
192
|
this.#host.setRestoringFromLocalStorage?.(true);
|
|
190
193
|
this.currentTime = maybeLoadedTime;
|
|
191
194
|
} else if (this.#currentTime === void 0) this.#currentTime = 0;
|
|
192
|
-
if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
|
|
193
195
|
}).catch((err) => {
|
|
194
196
|
if (!(err instanceof DOMException && err.name === "AbortError" || err instanceof Error && (err.name === "AbortError" || err.message.includes("signal is aborted") || err.message.includes("The user aborted a request")))) console.error("Error in PlaybackController hostConnected:", err);
|
|
195
197
|
});
|
|
@@ -206,7 +208,22 @@ var PlaybackController = class PlaybackController {
|
|
|
206
208
|
static {
|
|
207
209
|
this.THROTTLED_FRAME_TASK_MAX_WAITS = 100;
|
|
208
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Run frame rendering with throttling to prevent concurrent executions.
|
|
213
|
+
* Uses the new FrameController when available, falling back to frameTask.
|
|
214
|
+
*/
|
|
209
215
|
async runThrottledFrameTask() {
|
|
216
|
+
if (this.#host.frameController) {
|
|
217
|
+
try {
|
|
218
|
+
await this.#host.frameController.renderFrame(this.currentTimeMs, { onAnimationsUpdate: (root) => {
|
|
219
|
+
updateAnimations(root);
|
|
220
|
+
} });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
223
|
+
console.error("FrameController error:", error);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
210
227
|
if (this.#frameTaskInProgress) {
|
|
211
228
|
this.#pendingFrameTaskRun = true;
|
|
212
229
|
let waitLoopCount = 0;
|
|
@@ -341,8 +358,13 @@ var PlaybackController = class PlaybackController {
|
|
|
341
358
|
const startMs = logicalTimeMs;
|
|
342
359
|
const endMs = Math.min(logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS, toMs);
|
|
343
360
|
const willReachEnd = endMs >= toMs;
|
|
344
|
-
if (!host.renderAudio)
|
|
361
|
+
if (!host.renderAudio) {
|
|
362
|
+
console.log("[PlaybackController] host.renderAudio is not defined");
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
console.log(`[PlaybackController] Rendering audio from ${startMs}ms to ${endMs}ms`);
|
|
345
366
|
const audioBuffer = await host.renderAudio(startMs, endMs);
|
|
367
|
+
console.log(`[PlaybackController] Got audio buffer: ${audioBuffer.length} samples, ${audioBuffer.duration}s`);
|
|
346
368
|
bufferCount++;
|
|
347
369
|
const source = playbackContext.createBufferSource();
|
|
348
370
|
source.buffer = audioBuffer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.js","names":["#FPS","#host","#pendingSeekTime","#currentTime","#currentTimeMsProvider","#notifyListeners","#seekInProgress","#playingProvider","#playing","#loopProvider","#loop","#durationMsProvider","#processingPendingSeek","#removed","#frameTaskInProgress","#pendingFrameTaskRun","#processingPendingFrameTask","#listeners","#pendingAudioContext","#playbackAudioContext","rawTimeMs: number","#playbackWrapTimeSeconds","#loopingPlayback","#MS_PER_FRAME","#updatePlaybackTime","#playbackAnimationFrameRequest","#syncPlayheadToAudioContext","#AUDIO_PLAYBACK_SLICE_MS"],"sources":["../../src/gui/PlaybackController.ts"],"sourcesContent":["import { ContextProvider } from \"@lit/context\";\nimport { Task, TaskStatus } from \"@lit/task\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { EF_INTERACTIVE } from \"../EF_INTERACTIVE.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\ninterface PlaybackHost extends HTMLElement, ReactiveControllerHost {\n currentTimeMs: number;\n durationMs: number;\n endTimeMs: number;\n frameTask: { run(): void; taskComplete: Promise<unknown> };\n renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;\n waitForMediaDurations?(signal?: AbortSignal): Promise<void>;\n saveTimeToLocalStorage?(time: number): void;\n loadTimeFromLocalStorage?(): number | undefined;\n requestUpdate(property?: string): void;\n updateComplete: Promise<boolean>;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n playbackController?: PlaybackController;\n parentTimegroup?: any;\n rootTimegroup?: any;\n}\n\nexport type PlaybackControllerUpdateEvent = {\n property: \"playing\" | \"loop\" | \"currentTimeMs\";\n value: boolean | number;\n};\n\n/**\n * Manages playback state and audio-driven timing for root temporal elements\n *\n * Created automatically when a temporal element becomes a root (no parent timegroup)\n * Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants\n * Handles:\n * - Audio-driven playback with Web Audio API\n * - Seek and frame rendering throttling\n * - Time state management with pending seek handling\n * - Playback loop behavior\n *\n * Works with any temporal element (timegroups or standalone media) via PlaybackHost interface\n */\nexport class PlaybackController implements ReactiveController {\n #host: PlaybackHost;\n #playing = false;\n #loop = false;\n #listeners = new Set<(event: PlaybackControllerUpdateEvent) => void>();\n #playingProvider: ContextProvider<typeof playingContext>;\n #loopProvider: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider: ContextProvider<typeof currentTimeContext>;\n #durationMsProvider: ContextProvider<typeof durationContext>;\n\n #FPS = 30;\n #MS_PER_FRAME = 1000 / this.#FPS;\n #playbackAudioContext: AudioContext | null = null;\n #playbackAnimationFrameRequest: number | null = null;\n #pendingAudioContext: AudioContext | null = null;\n #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000;\n\n #frameTaskInProgress = false;\n #pendingFrameTaskRun = false;\n #processingPendingFrameTask = false;\n\n #currentTime: number | undefined = undefined;\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #loopingPlayback = false; // Track if we're in a looping playback session\n #playbackWrapTimeSeconds = 0; // The AudioContext time when we wrapped\n\n seekTask!: Task<readonly [number | undefined], number | undefined>;\n\n constructor(host: PlaybackHost) {\n this.#host = host;\n host.addController(this);\n\n this.seekTask = new Task(this.#host, {\n autoRun: false,\n args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,\n onComplete: () => {},\n task: async ([targetTime], { signal }) => {\n // Check abort before starting\n signal?.throwIfAborted();\n \n await this.#host.waitForMediaDurations?.(signal);\n \n // Check abort after async operation\n signal?.throwIfAborted();\n \n const newTime = Math.max(\n 0,\n Math.min(targetTime ?? 0, this.#host.durationMs / 1000),\n );\n this.#currentTime = newTime;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: this.currentTimeMs,\n });\n \n // Check abort before frame task\n signal?.throwIfAborted();\n \n await this.runThrottledFrameTask();\n \n // Check abort after frame task\n signal?.throwIfAborted();\n \n // Save to localStorage for persistence (only if not restoring to avoid loops)\n // Check if host has a restoration flag before saving\n // Use a method to check restoration state to avoid accessing private fields\n const isRestoring = (this.#host as any).isRestoringFromLocalStorage?.() ?? false;\n if (!isRestoring) {\n this.#host.saveTimeToLocalStorage?.(newTime);\n } else {\n // Clear restoration flag after seek completes\n (this.#host as any).setRestoringFromLocalStorage?.(false);\n }\n this.#seekInProgress = false;\n return newTime;\n },\n });\n\n this.#playingProvider = new ContextProvider(host, {\n context: playingContext,\n initialValue: this.#playing,\n });\n this.#loopProvider = new ContextProvider(host, {\n context: loopContext,\n initialValue: this.#loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(host, {\n context: currentTimeContext,\n initialValue: host.currentTimeMs,\n });\n this.#durationMsProvider = new ContextProvider(host, {\n context: durationContext,\n initialValue: host.durationMs,\n });\n }\n\n get currentTime(): number {\n const rawTime = this.#currentTime ?? 0;\n // Quantize to frame boundaries based on host's fps\n const fps = (this.#host as any).fps ?? 30;\n if (!fps || fps <= 0) return rawTime;\n const frameDurationS = 1 / fps;\n const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;\n // Clamp to valid range after quantization to prevent exceeding duration\n const durationS = this.#host.durationMs / 1000;\n return Math.max(0, Math.min(quantizedTime, durationS));\n }\n\n set currentTime(time: number) {\n time = Math.max(0, Math.min(this.#host.durationMs / 1000, time));\n if (Number.isNaN(time)) {\n return;\n }\n if (time === this.#currentTime && !this.#processingPendingSeek) {\n return;\n }\n if (this.#pendingSeekTime === time) {\n return;\n }\n\n if (this.#seekInProgress) {\n this.#pendingSeekTime = time;\n this.#currentTime = time;\n return;\n }\n\n this.#currentTime = time;\n this.#seekInProgress = true;\n\n this.seekTask.run().finally(() => {\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== time\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n get playing(): boolean {\n return this.#playing;\n }\n\n setPlaying(value: boolean): void {\n if (this.#playing === value) return;\n this.#playing = value;\n this.#playingProvider.setValue(value);\n this.#host.requestUpdate(\"playing\");\n this.#notifyListeners({ property: \"playing\", value });\n\n if (value) {\n this.startPlayback();\n } else {\n this.stopPlayback();\n }\n }\n\n get loop(): boolean {\n return this.#loop;\n }\n\n setLoop(value: boolean): void {\n if (this.#loop === value) return;\n this.#loop = value;\n this.#loopProvider.setValue(value);\n this.#host.requestUpdate(\"loop\");\n this.#notifyListeners({ property: \"loop\", value });\n }\n\n get currentTimeMs(): number {\n return this.currentTime * 1000;\n }\n\n setCurrentTimeMs(value: number): void {\n this.currentTime = value / 1000;\n }\n\n // Update time during playback without triggering a seek\n // Used by #syncPlayheadToAudioContext to avoid frame drops\n #updatePlaybackTime(timeMs: number): void {\n // Clamp to valid range to prevent time exceeding duration\n const durationMs = this.#host.durationMs;\n const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));\n const timeSec = clampedTimeMs / 1000;\n if (this.#currentTime === timeSec) {\n return;\n }\n this.#currentTime = timeSec;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(clampedTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: clampedTimeMs,\n });\n // Trigger frame rendering without the async seek mechanism\n this.runThrottledFrameTask();\n }\n\n play(): void {\n this.setPlaying(true);\n }\n\n pause(): void {\n this.setPlaying(false);\n }\n\n #removed = false;\n \n hostConnected(): void {\n // Defer all operations to avoid blocking during initialization\n // This prevents deadlocks when many timegroups are initializing simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n // Check if this controller was removed before the RAF callback executed.\n // This happens when wrapWithWorkbench moves the element, causing disconnect/reconnect.\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n \n if (this.#playing) {\n this.startPlayback();\n } else {\n // Wait for media durations then restore from localStorage\n this.#host.waitForMediaDurations?.().then(() => {\n // Check again after async operation - controller could have been removed\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n \n const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();\n if (maybeLoadedTime !== undefined) {\n // Set restoration flag to prevent seekTask from saving back to localStorage\n (this.#host as any).setRestoringFromLocalStorage?.(true);\n this.currentTime = maybeLoadedTime;\n // Flag is cleared by seekTask after seek finishes\n } else if (this.#currentTime === undefined) {\n this.#currentTime = 0;\n }\n if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {\n this.seekTask.run();\n }\n }).catch(err => {\n // Don't log AbortError - these are intentional cancellations when element is disconnected\n const isAbortError = \n err instanceof DOMException && err.name === \"AbortError\" ||\n err instanceof Error && (\n err.name === \"AbortError\" ||\n err.message.includes(\"signal is aborted\") ||\n err.message.includes(\"The user aborted a request\")\n );\n \n if (!isAbortError) {\n console.error(\"Error in PlaybackController hostConnected:\", err);\n }\n });\n }\n });\n });\n }\n\n hostDisconnected(): void {\n this.pause();\n }\n\n hostUpdated(): void {\n this.#durationMsProvider.setValue(this.#host.durationMs);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n\n static readonly THROTTLED_FRAME_TASK_MAX_WAITS = 100;\n \n async runThrottledFrameTask(): Promise<void> {\n if (this.#frameTaskInProgress) {\n this.#pendingFrameTaskRun = true;\n let waitLoopCount = 0;\n while (this.#frameTaskInProgress) {\n waitLoopCount++;\n if (waitLoopCount > PlaybackController.THROTTLED_FRAME_TASK_MAX_WAITS) {\n // Safety break to prevent infinite loops\n break;\n }\n await this.#host.frameTask.taskComplete;\n }\n return;\n }\n\n this.#frameTaskInProgress = true;\n\n try {\n await this.#host.frameTask.run();\n } catch (error) {\n console.error(\"Frame task error:\", error);\n } finally {\n this.#frameTaskInProgress = false;\n\n if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {\n this.#pendingFrameTaskRun = false;\n this.#processingPendingFrameTask = true;\n try {\n await this.runThrottledFrameTask();\n } finally {\n this.#processingPendingFrameTask = false;\n }\n } else {\n this.#pendingFrameTaskRun = false;\n }\n }\n }\n\n addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void {\n this.#listeners.add(listener);\n }\n\n removeListener(\n listener: (event: PlaybackControllerUpdateEvent) => void,\n ): void {\n this.#listeners.delete(listener);\n }\n\n #notifyListeners(event: PlaybackControllerUpdateEvent): void {\n for (const listener of this.#listeners) {\n listener(event);\n }\n }\n\n remove(): void {\n this.#removed = true; // Mark as removed to abort any pending RAF callbacks\n this.stopPlayback();\n this.#listeners.clear();\n this.#host.removeController(this);\n }\n\n setPendingAudioContext(context: AudioContext): void {\n this.#pendingAudioContext = context;\n }\n\n #syncPlayheadToAudioContext(startMs: number) {\n const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;\n const endMs = this.#host.endTimeMs;\n\n // Calculate raw time based on audio context\n let rawTimeMs: number;\n if (\n this.#playbackWrapTimeSeconds > 0 &&\n audioContextTime >= this.#playbackWrapTimeSeconds\n ) {\n // After wrap: time since wrap, wrapped to duration\n const timeSinceWrap =\n (audioContextTime - this.#playbackWrapTimeSeconds) * 1000;\n rawTimeMs = timeSinceWrap % endMs;\n } else {\n // Before wrap or no wrap: normal calculation\n rawTimeMs = startMs + audioContextTime * 1000;\n\n // If looping and we've reached the end, wrap around\n if (this.#loopingPlayback && rawTimeMs >= endMs) {\n rawTimeMs = rawTimeMs % endMs;\n }\n }\n\n const nextTimeMs =\n Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;\n\n // During playback, update time directly without triggering seek\n // This avoids frame drops at the loop boundary\n this.#updatePlaybackTime(nextTimeMs);\n\n // Only check for end if we haven't already handled looping\n if (!this.#loopingPlayback && nextTimeMs >= endMs) {\n this.maybeLoopPlayback();\n return;\n }\n\n this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {\n this.#syncPlayheadToAudioContext(startMs);\n });\n }\n\n private async maybeLoopPlayback() {\n if (this.#loop) {\n // Loop enabled: reset to beginning and restart playback\n // We restart the audio system directly without changing #playing state\n // to keep the play button in sync\n this.setCurrentTimeMs(0);\n // Restart in next frame without awaiting to minimize gap\n requestAnimationFrame(() => {\n this.startPlayback();\n });\n } else {\n // No loop: reset to beginning and stop\n // This ensures play button works when clicked again\n this.setCurrentTimeMs(0);\n this.pause();\n }\n }\n\n private async stopPlayback() {\n if (this.#playbackAudioContext) {\n if (this.#playbackAudioContext.state !== \"closed\") {\n await this.#playbackAudioContext.close();\n }\n }\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#playbackAudioContext = null;\n this.#playbackAnimationFrameRequest = null;\n this.#pendingAudioContext = null;\n }\n\n private async startPlayback() {\n // Guard against starting playback on a removed controller\n if (this.#removed) {\n return;\n }\n \n await this.stopPlayback();\n const host = this.#host;\n if (!host) {\n return;\n }\n\n if (host.waitForMediaDurations) {\n await host.waitForMediaDurations();\n }\n \n // Check again after async - controller could have been removed\n if (this.#removed) {\n return;\n }\n\n const currentMs = this.currentTimeMs;\n const fromMs = currentMs;\n const toMs = host.endTimeMs;\n\n if (fromMs >= toMs) {\n this.pause();\n return;\n }\n\n let bufferCount = 0;\n // Check for pre-resumed AudioContext from synchronous user interaction\n if (this.#pendingAudioContext) {\n this.#playbackAudioContext = this.#pendingAudioContext;\n this.#pendingAudioContext = null;\n } else {\n this.#playbackAudioContext = new AudioContext({\n latencyHint: \"playback\",\n });\n }\n this.#loopingPlayback = this.#loop; // Remember if we're in a looping session\n this.#playbackWrapTimeSeconds = 0; // Reset wrap time\n\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#syncPlayheadToAudioContext(currentMs);\n const playbackContext = this.#playbackAudioContext;\n\n // Check if context is suspended (fallback for newly-created contexts)\n if (playbackContext.state === \"suspended\") {\n // Attempt to resume (may not work on mobile if user interaction context is lost)\n try {\n await playbackContext.resume();\n // Check state again after resume attempt\n if (playbackContext.state === \"suspended\") {\n console.warn(\n \"AudioContext is suspended and resume() failed. \" +\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler. \" +\n \"Media playback will not work until user has interacted with page.\",\n );\n this.setPlaying(false);\n return;\n }\n } catch (error) {\n console.warn(\n \"Failed to resume AudioContext:\",\n error,\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler.\",\n );\n this.setPlaying(false);\n return;\n }\n }\n await playbackContext.suspend();\n\n // Track the logical media time (what position in the media we're rendering)\n // vs the AudioContext schedule time (when to play it)\n let logicalTimeMs = currentMs;\n let audioContextTimeMs = 0; // Tracks the schedule position in the AudioContext timeline\n let hasWrapped = false;\n\n const fillBuffer = async () => {\n if (bufferCount > 2) {\n return;\n }\n const canFillBuffer = await queueBufferSource();\n if (canFillBuffer) {\n fillBuffer();\n }\n };\n\n const queueBufferSource = async () => {\n // Check if we've already wrapped and aren't looping anymore\n if (hasWrapped && !this.#loopingPlayback) {\n return false;\n }\n\n const startMs = logicalTimeMs;\n const endMs = Math.min(\n logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS,\n toMs,\n );\n\n // Will this slice reach the end?\n const willReachEnd = endMs >= toMs;\n\n if (!host.renderAudio) {\n return false;\n }\n\n const audioBuffer = await host.renderAudio(startMs, endMs);\n bufferCount++;\n const source = playbackContext.createBufferSource();\n source.buffer = audioBuffer;\n source.connect(playbackContext.destination);\n // Schedule this buffer to play at the current audioContextTime position\n source.start(audioContextTimeMs / 1000);\n\n const sliceDurationMs = endMs - startMs;\n\n source.onended = () => {\n bufferCount--;\n\n if (willReachEnd) {\n if (!this.#loopingPlayback) {\n // Not looping, end playback\n this.maybeLoopPlayback();\n } else {\n // Looping: continue filling buffer after wrap\n fillBuffer();\n }\n } else {\n // Continue filling buffer\n fillBuffer();\n }\n };\n\n // Advance the AudioContext schedule time\n audioContextTimeMs += sliceDurationMs;\n\n // If this buffer reaches the end and we're looping, immediately queue the wraparound\n if (willReachEnd && this.#loopingPlayback) {\n // Mark that we've wrapped\n hasWrapped = true;\n // Store when we wrapped (relative to when playback started, which is time 0 in AudioContext)\n // This is the duration from start to end\n this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1000;\n // Reset logical time to beginning\n logicalTimeMs = 0;\n // Continue buffering will happen in fillBuffer() call below\n } else {\n // Normal advance\n logicalTimeMs = endMs;\n }\n\n return true;\n };\n\n try {\n await fillBuffer();\n await playbackContext.resume();\n } catch (error) {\n // Ignore errors if AudioContext is closed or during test cleanup\n if (\n error instanceof Error &&\n (error.name === \"InvalidStateError\" || error.message.includes(\"closed\"))\n ) {\n return;\n }\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlaybackController.js","names":["#FPS","#host","#playingProvider","#playing","#loopProvider","#loop","#currentTimeMsProvider","#durationMsProvider","#currentTime","#processingPendingSeek","#pendingSeekTime","#seekInProgress","#runSeek","#seekAbortController","#notifyListeners","#removed","#frameTaskInProgress","#pendingFrameTaskRun","#processingPendingFrameTask","#listeners","#pendingAudioContext","#playbackAudioContext","rawTimeMs: number","#playbackWrapTimeSeconds","#loopingPlayback","#MS_PER_FRAME","#updatePlaybackTime","#playbackAnimationFrameRequest","#syncPlayheadToAudioContext","#AUDIO_PLAYBACK_SLICE_MS"],"sources":["../../src/gui/PlaybackController.ts"],"sourcesContent":["import { ContextProvider } from \"@lit/context\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport { updateAnimations, type AnimatableElement } from \"../elements/updateAnimations.js\";\nimport type { RenderFrameOptions } from \"../preview/FrameController.js\";\n\ninterface PlaybackHost extends HTMLElement, ReactiveControllerHost {\n currentTimeMs: number;\n durationMs: number;\n endTimeMs: number;\n frameTask: { run(): void; taskComplete: Promise<unknown> };\n /** New centralized frame controller (replaces frameTask) */\n frameController?: { \n renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<void>; \n abort(): void;\n };\n renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;\n waitForMediaDurations?(signal?: AbortSignal): Promise<void>;\n saveTimeToLocalStorage?(time: number): void;\n loadTimeFromLocalStorage?(): number | undefined;\n requestUpdate(property?: string): void;\n updateComplete: Promise<boolean>;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n playbackController?: PlaybackController;\n parentTimegroup?: any;\n rootTimegroup?: any;\n}\n\nexport type PlaybackControllerUpdateEvent = {\n property: \"playing\" | \"loop\" | \"currentTimeMs\";\n value: boolean | number;\n};\n\n/**\n * Manages playback state and audio-driven timing for root temporal elements\n *\n * Created automatically when a temporal element becomes a root (no parent timegroup)\n * Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants\n * Handles:\n * - Audio-driven playback with Web Audio API\n * - Seek and frame rendering throttling\n * - Time state management with pending seek handling\n * - Playback loop behavior\n *\n * Works with any temporal element (timegroups or standalone media) via PlaybackHost interface\n */\nexport class PlaybackController implements ReactiveController {\n #host: PlaybackHost;\n #playing = false;\n #loop = false;\n #listeners = new Set<(event: PlaybackControllerUpdateEvent) => void>();\n #playingProvider: ContextProvider<typeof playingContext>;\n #loopProvider: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider: ContextProvider<typeof currentTimeContext>;\n #durationMsProvider: ContextProvider<typeof durationContext>;\n\n #FPS = 30;\n #MS_PER_FRAME = 1000 / this.#FPS;\n #playbackAudioContext: AudioContext | null = null;\n #playbackAnimationFrameRequest: number | null = null;\n #pendingAudioContext: AudioContext | null = null;\n #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000;\n\n #frameTaskInProgress = false;\n #pendingFrameTaskRun = false;\n #processingPendingFrameTask = false;\n\n #currentTime: number | undefined = undefined;\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #loopingPlayback = false; // Track if we're in a looping playback session\n #playbackWrapTimeSeconds = 0; // The AudioContext time when we wrapped\n\n #seekAbortController: AbortController | null = null;\n\n constructor(host: PlaybackHost) {\n this.#host = host;\n host.addController(this);\n\n this.#playingProvider = new ContextProvider(host, {\n context: playingContext,\n initialValue: this.#playing,\n });\n this.#loopProvider = new ContextProvider(host, {\n context: loopContext,\n initialValue: this.#loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(host, {\n context: currentTimeContext,\n initialValue: host.currentTimeMs,\n });\n this.#durationMsProvider = new ContextProvider(host, {\n context: durationContext,\n initialValue: host.durationMs,\n });\n }\n\n get currentTime(): number {\n const rawTime = this.#currentTime ?? 0;\n // Quantize to frame boundaries based on host's fps\n const fps = (this.#host as any).fps ?? 30;\n if (!fps || fps <= 0) return rawTime;\n const frameDurationS = 1 / fps;\n const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;\n // Clamp to valid range after quantization to prevent exceeding duration\n const durationS = this.#host.durationMs / 1000;\n return Math.max(0, Math.min(quantizedTime, durationS));\n }\n\n set currentTime(time: number) {\n time = Math.max(0, Math.min(this.#host.durationMs / 1000, time));\n if (Number.isNaN(time)) {\n return;\n }\n if (time === this.#currentTime && !this.#processingPendingSeek) {\n return;\n }\n if (this.#pendingSeekTime === time) {\n return;\n }\n\n if (this.#seekInProgress) {\n this.#pendingSeekTime = time;\n this.#currentTime = time;\n return;\n }\n\n this.#currentTime = time;\n this.#seekInProgress = true;\n\n this.#runSeek(time).finally(() => {\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== time\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n async #runSeek(targetTime: number): Promise<number | undefined> {\n // Abort any in-flight seek\n this.#seekAbortController?.abort();\n this.#seekAbortController = new AbortController();\n const signal = this.#seekAbortController.signal;\n\n try {\n signal.throwIfAborted();\n \n await this.#host.waitForMediaDurations?.(signal);\n signal.throwIfAborted();\n \n const newTime = Math.max(\n 0,\n Math.min(targetTime, this.#host.durationMs / 1000),\n );\n this.#currentTime = newTime;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: this.currentTimeMs,\n });\n \n signal.throwIfAborted();\n \n await this.runThrottledFrameTask();\n signal.throwIfAborted();\n \n // Save to localStorage for persistence (only if not restoring to avoid loops)\n const isRestoring = (this.#host as any).isRestoringFromLocalStorage?.() ?? false;\n if (!isRestoring) {\n this.#host.saveTimeToLocalStorage?.(newTime);\n } else {\n (this.#host as any).setRestoringFromLocalStorage?.(false);\n }\n this.#seekInProgress = false;\n return newTime;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n // Expected - don't log\n return undefined;\n }\n throw error;\n }\n }\n\n get playing(): boolean {\n return this.#playing;\n }\n\n setPlaying(value: boolean): void {\n if (this.#playing === value) return;\n this.#playing = value;\n this.#playingProvider.setValue(value);\n this.#host.requestUpdate(\"playing\");\n this.#notifyListeners({ property: \"playing\", value });\n\n if (value) {\n this.startPlayback();\n } else {\n this.stopPlayback();\n }\n }\n\n get loop(): boolean {\n return this.#loop;\n }\n\n setLoop(value: boolean): void {\n if (this.#loop === value) return;\n this.#loop = value;\n this.#loopProvider.setValue(value);\n this.#host.requestUpdate(\"loop\");\n this.#notifyListeners({ property: \"loop\", value });\n }\n\n get currentTimeMs(): number {\n return this.currentTime * 1000;\n }\n\n setCurrentTimeMs(value: number): void {\n this.currentTime = value / 1000;\n }\n\n // Update time during playback without triggering a seek\n // Used by #syncPlayheadToAudioContext to avoid frame drops\n #updatePlaybackTime(timeMs: number): void {\n // Clamp to valid range to prevent time exceeding duration\n const durationMs = this.#host.durationMs;\n const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));\n const timeSec = clampedTimeMs / 1000;\n if (this.#currentTime === timeSec) {\n return;\n }\n this.#currentTime = timeSec;\n this.#host.requestUpdate(\"currentTime\");\n this.#currentTimeMsProvider.setValue(clampedTimeMs);\n this.#notifyListeners({\n property: \"currentTimeMs\",\n value: clampedTimeMs,\n });\n // Trigger frame rendering without the async seek mechanism\n this.runThrottledFrameTask();\n }\n\n play(): void {\n this.setPlaying(true);\n }\n\n pause(): void {\n this.setPlaying(false);\n }\n\n #removed = false;\n \n hostConnected(): void {\n // Defer all operations to avoid blocking during initialization\n // This prevents deadlocks when many timegroups are initializing simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n // Check if this controller was removed before the RAF callback executed.\n // This happens when wrapWithWorkbench moves the element, causing disconnect/reconnect.\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n \n if (this.#playing) {\n this.startPlayback();\n } else {\n // Wait for media durations then restore from localStorage\n this.#host.waitForMediaDurations?.().then(() => {\n // Check again after async operation - controller could have been removed\n if (this.#removed || this.#host.playbackController !== this) {\n return;\n }\n \n const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();\n if (maybeLoadedTime !== undefined) {\n // Set restoration flag to prevent seek from saving back to localStorage\n (this.#host as any).setRestoringFromLocalStorage?.(true);\n this.currentTime = maybeLoadedTime;\n // Flag is cleared by runSeek after seek finishes\n } else if (this.#currentTime === undefined) {\n this.#currentTime = 0;\n }\n }).catch(err => {\n // Don't log AbortError - these are intentional cancellations when element is disconnected\n const isAbortError = \n err instanceof DOMException && err.name === \"AbortError\" ||\n err instanceof Error && (\n err.name === \"AbortError\" ||\n err.message.includes(\"signal is aborted\") ||\n err.message.includes(\"The user aborted a request\")\n );\n \n if (!isAbortError) {\n console.error(\"Error in PlaybackController hostConnected:\", err);\n }\n });\n }\n });\n });\n }\n\n hostDisconnected(): void {\n this.pause();\n }\n\n hostUpdated(): void {\n this.#durationMsProvider.setValue(this.#host.durationMs);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n\n static readonly THROTTLED_FRAME_TASK_MAX_WAITS = 100;\n \n /**\n * Run frame rendering with throttling to prevent concurrent executions.\n * Uses the new FrameController when available, falling back to frameTask.\n */\n async runThrottledFrameTask(): Promise<void> {\n // Use FrameController if available (new centralized system)\n if (this.#host.frameController) {\n // FrameController handles its own cancellation and queuing internally\n // Animation updates are centralized via the onAnimationsUpdate callback\n try {\n await this.#host.frameController.renderFrame(this.currentTimeMs, {\n onAnimationsUpdate: (root: Element) => {\n // Update CSS visibility and animation synchronization after frame renders\n // This sets display:none on elements outside their time range\n updateAnimations(root as unknown as AnimatableElement);\n },\n });\n } catch (error) {\n // Silently ignore AbortErrors (expected during cancellation)\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n console.error(\"FrameController error:\", error);\n }\n return;\n }\n\n // Fallback to old frameTask system (for backwards compatibility)\n if (this.#frameTaskInProgress) {\n this.#pendingFrameTaskRun = true;\n let waitLoopCount = 0;\n while (this.#frameTaskInProgress) {\n waitLoopCount++;\n if (waitLoopCount > PlaybackController.THROTTLED_FRAME_TASK_MAX_WAITS) {\n // Safety break to prevent infinite loops\n break;\n }\n await this.#host.frameTask.taskComplete;\n }\n return;\n }\n\n this.#frameTaskInProgress = true;\n\n try {\n await this.#host.frameTask.run();\n } catch (error) {\n console.error(\"Frame task error:\", error);\n } finally {\n this.#frameTaskInProgress = false;\n\n if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {\n this.#pendingFrameTaskRun = false;\n this.#processingPendingFrameTask = true;\n try {\n await this.runThrottledFrameTask();\n } finally {\n this.#processingPendingFrameTask = false;\n }\n } else {\n this.#pendingFrameTaskRun = false;\n }\n }\n }\n\n addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void {\n this.#listeners.add(listener);\n }\n\n removeListener(\n listener: (event: PlaybackControllerUpdateEvent) => void,\n ): void {\n this.#listeners.delete(listener);\n }\n\n #notifyListeners(event: PlaybackControllerUpdateEvent): void {\n for (const listener of this.#listeners) {\n listener(event);\n }\n }\n\n remove(): void {\n this.#removed = true; // Mark as removed to abort any pending RAF callbacks\n this.stopPlayback();\n this.#listeners.clear();\n this.#host.removeController(this);\n }\n\n setPendingAudioContext(context: AudioContext): void {\n this.#pendingAudioContext = context;\n }\n\n #syncPlayheadToAudioContext(startMs: number) {\n const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;\n const endMs = this.#host.endTimeMs;\n\n // Calculate raw time based on audio context\n let rawTimeMs: number;\n if (\n this.#playbackWrapTimeSeconds > 0 &&\n audioContextTime >= this.#playbackWrapTimeSeconds\n ) {\n // After wrap: time since wrap, wrapped to duration\n const timeSinceWrap =\n (audioContextTime - this.#playbackWrapTimeSeconds) * 1000;\n rawTimeMs = timeSinceWrap % endMs;\n } else {\n // Before wrap or no wrap: normal calculation\n rawTimeMs = startMs + audioContextTime * 1000;\n\n // If looping and we've reached the end, wrap around\n if (this.#loopingPlayback && rawTimeMs >= endMs) {\n rawTimeMs = rawTimeMs % endMs;\n }\n }\n\n const nextTimeMs =\n Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;\n\n // During playback, update time directly without triggering seek\n // This avoids frame drops at the loop boundary\n this.#updatePlaybackTime(nextTimeMs);\n\n // Only check for end if we haven't already handled looping\n if (!this.#loopingPlayback && nextTimeMs >= endMs) {\n this.maybeLoopPlayback();\n return;\n }\n\n this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {\n this.#syncPlayheadToAudioContext(startMs);\n });\n }\n\n private async maybeLoopPlayback() {\n if (this.#loop) {\n // Loop enabled: reset to beginning and restart playback\n // We restart the audio system directly without changing #playing state\n // to keep the play button in sync\n this.setCurrentTimeMs(0);\n // Restart in next frame without awaiting to minimize gap\n requestAnimationFrame(() => {\n this.startPlayback();\n });\n } else {\n // No loop: reset to beginning and stop\n // This ensures play button works when clicked again\n this.setCurrentTimeMs(0);\n this.pause();\n }\n }\n\n private async stopPlayback() {\n if (this.#playbackAudioContext) {\n if (this.#playbackAudioContext.state !== \"closed\") {\n await this.#playbackAudioContext.close();\n }\n }\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#playbackAudioContext = null;\n this.#playbackAnimationFrameRequest = null;\n this.#pendingAudioContext = null;\n }\n\n private async startPlayback() {\n // Guard against starting playback on a removed controller\n if (this.#removed) {\n return;\n }\n \n await this.stopPlayback();\n const host = this.#host;\n if (!host) {\n return;\n }\n\n if (host.waitForMediaDurations) {\n await host.waitForMediaDurations();\n }\n \n // Check again after async - controller could have been removed\n if (this.#removed) {\n return;\n }\n\n const currentMs = this.currentTimeMs;\n const fromMs = currentMs;\n const toMs = host.endTimeMs;\n\n if (fromMs >= toMs) {\n this.pause();\n return;\n }\n\n let bufferCount = 0;\n // Check for pre-resumed AudioContext from synchronous user interaction\n if (this.#pendingAudioContext) {\n this.#playbackAudioContext = this.#pendingAudioContext;\n this.#pendingAudioContext = null;\n } else {\n this.#playbackAudioContext = new AudioContext({\n latencyHint: \"playback\",\n });\n }\n this.#loopingPlayback = this.#loop; // Remember if we're in a looping session\n this.#playbackWrapTimeSeconds = 0; // Reset wrap time\n\n if (this.#playbackAnimationFrameRequest) {\n cancelAnimationFrame(this.#playbackAnimationFrameRequest);\n }\n this.#syncPlayheadToAudioContext(currentMs);\n const playbackContext = this.#playbackAudioContext;\n\n // Check if context is suspended (fallback for newly-created contexts)\n if (playbackContext.state === \"suspended\") {\n // Attempt to resume (may not work on mobile if user interaction context is lost)\n try {\n await playbackContext.resume();\n // Check state again after resume attempt\n if (playbackContext.state === \"suspended\") {\n console.warn(\n \"AudioContext is suspended and resume() failed. \" +\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler. \" +\n \"Media playback will not work until user has interacted with page.\",\n );\n this.setPlaying(false);\n return;\n }\n } catch (error) {\n console.warn(\n \"Failed to resume AudioContext:\",\n error,\n \"On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler.\",\n );\n this.setPlaying(false);\n return;\n }\n }\n await playbackContext.suspend();\n\n // Track the logical media time (what position in the media we're rendering)\n // vs the AudioContext schedule time (when to play it)\n let logicalTimeMs = currentMs;\n let audioContextTimeMs = 0; // Tracks the schedule position in the AudioContext timeline\n let hasWrapped = false;\n\n const fillBuffer = async () => {\n if (bufferCount > 2) {\n return;\n }\n const canFillBuffer = await queueBufferSource();\n if (canFillBuffer) {\n fillBuffer();\n }\n };\n\n const queueBufferSource = async () => {\n // Check if we've already wrapped and aren't looping anymore\n if (hasWrapped && !this.#loopingPlayback) {\n return false;\n }\n\n const startMs = logicalTimeMs;\n const endMs = Math.min(\n logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS,\n toMs,\n );\n\n // Will this slice reach the end?\n const willReachEnd = endMs >= toMs;\n\n if (!host.renderAudio) {\n console.log('[PlaybackController] host.renderAudio is not defined');\n return false;\n }\n\n console.log(`[PlaybackController] Rendering audio from ${startMs}ms to ${endMs}ms`);\n const audioBuffer = await host.renderAudio(startMs, endMs);\n console.log(`[PlaybackController] Got audio buffer: ${audioBuffer.length} samples, ${audioBuffer.duration}s`);\n bufferCount++;\n const source = playbackContext.createBufferSource();\n source.buffer = audioBuffer;\n source.connect(playbackContext.destination);\n // Schedule this buffer to play at the current audioContextTime position\n source.start(audioContextTimeMs / 1000);\n\n const sliceDurationMs = endMs - startMs;\n\n source.onended = () => {\n bufferCount--;\n\n if (willReachEnd) {\n if (!this.#loopingPlayback) {\n // Not looping, end playback\n this.maybeLoopPlayback();\n } else {\n // Looping: continue filling buffer after wrap\n fillBuffer();\n }\n } else {\n // Continue filling buffer\n fillBuffer();\n }\n };\n\n // Advance the AudioContext schedule time\n audioContextTimeMs += sliceDurationMs;\n\n // If this buffer reaches the end and we're looping, immediately queue the wraparound\n if (willReachEnd && this.#loopingPlayback) {\n // Mark that we've wrapped\n hasWrapped = true;\n // Store when we wrapped (relative to when playback started, which is time 0 in AudioContext)\n // This is the duration from start to end\n this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1000;\n // Reset logical time to beginning\n logicalTimeMs = 0;\n // Continue buffering will happen in fillBuffer() call below\n } else {\n // Normal advance\n logicalTimeMs = endMs;\n }\n\n return true;\n };\n\n try {\n await fillBuffer();\n await playbackContext.resume();\n } catch (error) {\n // Ignore errors if AudioContext is closed or during test cleanup\n if (\n error instanceof Error &&\n (error.name === \"InvalidStateError\" || error.message.includes(\"closed\"))\n ) {\n return;\n }\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|