@editframe/elements 0.20.4-beta.0 → 0.23.6-beta.0
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/DelayedLoadingState.js +0 -27
- package/dist/EF_FRAMEGEN.d.ts +5 -3
- package/dist/EF_FRAMEGEN.js +49 -11
- package/dist/_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js +7 -0
- package/dist/attachContextRoot.d.ts +1 -0
- package/dist/attachContextRoot.js +9 -0
- package/dist/elements/ContextProxiesController.d.ts +1 -2
- package/dist/elements/EFAudio.js +5 -9
- package/dist/elements/EFCaptions.d.ts +1 -3
- package/dist/elements/EFCaptions.js +112 -129
- package/dist/elements/EFImage.js +6 -7
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -5
- package/dist/elements/EFMedia/AssetMediaEngine.js +36 -33
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -78
- package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +7 -13
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +2 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +9 -25
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -17
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -18
- package/dist/elements/EFMedia.d.ts +19 -0
- package/dist/elements/EFMedia.js +44 -25
- package/dist/elements/EFSourceMixin.js +5 -7
- package/dist/elements/EFSurface.js +6 -9
- package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
- package/dist/elements/EFTemporal.d.ts +10 -0
- package/dist/elements/EFTemporal.js +100 -41
- package/dist/elements/EFThumbnailStrip.js +23 -73
- package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
- package/dist/elements/EFTimegroup.d.ts +35 -14
- package/dist/elements/EFTimegroup.js +138 -181
- package/dist/elements/EFVideo.d.ts +16 -2
- package/dist/elements/EFVideo.js +156 -108
- package/dist/elements/EFWaveform.js +23 -40
- package/dist/elements/SampleBuffer.js +3 -7
- package/dist/elements/TargetController.js +5 -5
- package/dist/elements/durationConverter.js +4 -4
- package/dist/elements/renderTemporalAudio.d.ts +10 -0
- package/dist/elements/renderTemporalAudio.js +35 -0
- package/dist/elements/updateAnimations.js +19 -43
- package/dist/gui/ContextMixin.d.ts +5 -5
- package/dist/gui/ContextMixin.js +167 -162
- package/dist/gui/Controllable.browsertest.d.ts +0 -0
- package/dist/gui/Controllable.d.ts +15 -0
- package/dist/gui/Controllable.js +9 -0
- package/dist/gui/EFConfiguration.js +7 -7
- package/dist/gui/EFControls.browsertest.d.ts +11 -0
- package/dist/gui/EFControls.d.ts +18 -4
- package/dist/gui/EFControls.js +70 -28
- package/dist/gui/EFDial.browsertest.d.ts +0 -0
- package/dist/gui/EFDial.d.ts +18 -0
- package/dist/gui/EFDial.js +141 -0
- package/dist/gui/EFFilmstrip.browsertest.d.ts +11 -0
- package/dist/gui/EFFilmstrip.d.ts +12 -2
- package/dist/gui/EFFilmstrip.js +214 -129
- package/dist/gui/EFFitScale.js +5 -8
- package/dist/gui/EFFocusOverlay.js +4 -4
- package/dist/gui/EFPause.browsertest.d.ts +0 -0
- package/dist/gui/EFPause.d.ts +23 -0
- package/dist/gui/EFPause.js +59 -0
- package/dist/gui/EFPlay.browsertest.d.ts +0 -0
- package/dist/gui/EFPlay.d.ts +23 -0
- package/dist/gui/EFPlay.js +59 -0
- package/dist/gui/EFPreview.d.ts +4 -0
- package/dist/gui/EFPreview.js +18 -9
- package/dist/gui/EFResizableBox.browsertest.d.ts +0 -0
- package/dist/gui/EFResizableBox.d.ts +34 -0
- package/dist/gui/EFResizableBox.js +547 -0
- package/dist/gui/EFScrubber.d.ts +9 -3
- package/dist/gui/EFScrubber.js +13 -13
- package/dist/gui/EFTimeDisplay.d.ts +7 -1
- package/dist/gui/EFTimeDisplay.js +8 -8
- package/dist/gui/EFToggleLoop.d.ts +9 -3
- package/dist/gui/EFToggleLoop.js +7 -5
- package/dist/gui/EFTogglePlay.d.ts +12 -4
- package/dist/gui/EFTogglePlay.js +26 -21
- package/dist/gui/EFWorkbench.js +5 -5
- package/dist/gui/PlaybackController.d.ts +67 -0
- package/dist/gui/PlaybackController.js +310 -0
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin2.js +1 -1
- package/dist/gui/TargetOrContextMixin.d.ts +10 -0
- package/dist/gui/TargetOrContextMixin.js +98 -0
- package/dist/gui/efContext.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -1
- package/dist/otel/BridgeSpanExporter.d.ts +13 -0
- package/dist/otel/BridgeSpanExporter.js +87 -0
- package/dist/otel/setupBrowserTracing.d.ts +12 -0
- package/dist/otel/setupBrowserTracing.js +32 -0
- package/dist/otel/tracingHelpers.d.ts +34 -0
- package/dist/otel/tracingHelpers.js +112 -0
- package/dist/style.css +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
- package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
- package/dist/transcoding/utils/UrlGenerator.js +2 -19
- package/dist/utils/LRUCache.js +6 -53
- package/package.json +13 -5
- package/src/elements/ContextProxiesController.ts +10 -10
- package/src/elements/EFAudio.ts +1 -0
- package/src/elements/EFCaptions.browsertest.ts +128 -56
- package/src/elements/EFCaptions.ts +60 -34
- package/src/elements/EFImage.browsertest.ts +1 -2
- package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
- package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
- package/src/elements/EFMedia.browsertest.ts +8 -15
- package/src/elements/EFMedia.ts +54 -8
- package/src/elements/EFSurface.browsertest.ts +2 -6
- package/src/elements/EFSurface.ts +1 -0
- package/src/elements/EFTemporal.browsertest.ts +58 -1
- package/src/elements/EFTemporal.ts +140 -4
- package/src/elements/EFThumbnailStrip.browsertest.ts +2 -8
- package/src/elements/EFThumbnailStrip.ts +1 -0
- package/src/elements/EFTimegroup.browsertest.ts +16 -15
- package/src/elements/EFTimegroup.ts +281 -275
- package/src/elements/EFVideo.browsertest.ts +162 -74
- package/src/elements/EFVideo.ts +229 -101
- package/src/elements/FetchContext.browsertest.ts +7 -2
- package/src/elements/TargetController.browsertest.ts +1 -0
- package/src/elements/TargetController.ts +1 -0
- package/src/elements/renderTemporalAudio.ts +108 -0
- package/src/elements/updateAnimations.browsertest.ts +181 -6
- package/src/elements/updateAnimations.ts +6 -6
- package/src/gui/ContextMixin.browsertest.ts +274 -27
- package/src/gui/ContextMixin.ts +230 -175
- package/src/gui/Controllable.browsertest.ts +258 -0
- package/src/gui/Controllable.ts +41 -0
- package/src/gui/EFControls.browsertest.ts +294 -80
- package/src/gui/EFControls.ts +139 -28
- package/src/gui/EFDial.browsertest.ts +84 -0
- package/src/gui/EFDial.ts +172 -0
- package/src/gui/EFFilmstrip.browsertest.ts +712 -0
- package/src/gui/EFFilmstrip.ts +213 -23
- package/src/gui/EFPause.browsertest.ts +202 -0
- package/src/gui/EFPause.ts +73 -0
- package/src/gui/EFPlay.browsertest.ts +202 -0
- package/src/gui/EFPlay.ts +73 -0
- package/src/gui/EFPreview.ts +20 -5
- package/src/gui/EFResizableBox.browsertest.ts +79 -0
- package/src/gui/EFResizableBox.ts +898 -0
- package/src/gui/EFScrubber.ts +7 -5
- package/src/gui/EFTimeDisplay.browsertest.ts +19 -19
- package/src/gui/EFTimeDisplay.ts +3 -1
- package/src/gui/EFToggleLoop.ts +6 -5
- package/src/gui/EFTogglePlay.ts +30 -23
- package/src/gui/PlaybackController.ts +522 -0
- package/src/gui/TWMixin.css +3 -0
- package/src/gui/TargetOrContextMixin.ts +185 -0
- package/src/gui/efContext.ts +2 -2
- package/src/otel/BridgeSpanExporter.ts +150 -0
- package/src/otel/setupBrowserTracing.ts +73 -0
- package/src/otel/tracingHelpers.ts +251 -0
- package/test/cache-integration-verification.browsertest.ts +1 -1
- package/types.json +1 -1
- package/dist/elements/ContextProxiesController.js +0 -69
|
@@ -5,10 +5,12 @@ import { css, html, LitElement, type PropertyValues } from "lit";
|
|
|
5
5
|
import { customElement, property } from "lit/decorators.js";
|
|
6
6
|
|
|
7
7
|
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
8
|
+
import { EF_RENDERING } from "../EF_RENDERING.js";
|
|
8
9
|
import { isContextMixin } from "../gui/ContextMixin.js";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { efContext } from "../gui/efContext.js";
|
|
11
|
+
import { TWMixin } from "../gui/TWMixin.js";
|
|
12
|
+
import { isTracingEnabled, withSpan } from "../otel/tracingHelpers.js";
|
|
13
|
+
import { deepGetMediaElements, type EFMedia } from "./EFMedia.js";
|
|
12
14
|
import {
|
|
13
15
|
deepGetElementsWithFrameTasks,
|
|
14
16
|
EFTemporal,
|
|
@@ -17,12 +19,19 @@ import {
|
|
|
17
19
|
shallowGetTemporalElements,
|
|
18
20
|
timegroupContext,
|
|
19
21
|
} from "./EFTemporal.js";
|
|
22
|
+
import { parseTimeToMs } from "./parseTimeToMs.js";
|
|
23
|
+
import { renderTemporalAudio } from "./renderTemporalAudio.js";
|
|
24
|
+
import { EFTargetable } from "./TargetController.js";
|
|
20
25
|
import { TimegroupController } from "./TimegroupController.js";
|
|
21
26
|
import {
|
|
22
27
|
evaluateTemporalStateForAnimation,
|
|
23
28
|
updateAnimations,
|
|
24
29
|
} from "./updateAnimations.ts";
|
|
25
30
|
|
|
31
|
+
declare global {
|
|
32
|
+
var EF_DEV_WORKBENCH: boolean | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
const log = debug("ef:elements:EFTimegroup");
|
|
27
36
|
|
|
28
37
|
// Cache for sequence mode duration calculations to avoid O(n) recalculation
|
|
@@ -47,13 +56,24 @@ export const shallowGetTimegroups = (
|
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
@customElement("ef-timegroup")
|
|
50
|
-
export class EFTimegroup extends EFTemporal(LitElement) {
|
|
59
|
+
export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
|
|
60
|
+
static get observedAttributes(): string[] {
|
|
61
|
+
// biome-ignore lint/complexity/noThisInStatic: It's okay to use this here
|
|
62
|
+
const parentAttributes = super.observedAttributes || [];
|
|
63
|
+
return [...parentAttributes, "mode", "overlap", "currenttime", "fit"];
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
static styles = css`
|
|
52
67
|
:host {
|
|
53
68
|
display: block;
|
|
69
|
+
position: relative;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
::slotted(ef-timegroup) {
|
|
74
|
+
position: absolute;
|
|
54
75
|
width: 100%;
|
|
55
76
|
height: 100%;
|
|
56
|
-
position: absolute;
|
|
57
77
|
top: 0;
|
|
58
78
|
left: 0;
|
|
59
79
|
}
|
|
@@ -62,95 +82,51 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
62
82
|
@provide({ context: timegroupContext })
|
|
63
83
|
_timeGroupContext = this;
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@property({
|
|
68
|
-
type: String,
|
|
69
|
-
attribute: "mode",
|
|
70
|
-
})
|
|
71
|
-
set mode(value: "fit" | "fixed" | "sequence" | "contain") {
|
|
72
|
-
// Invalidate duration cache when mode changes
|
|
73
|
-
sequenceDurationCache.delete(this);
|
|
74
|
-
this._mode = value;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
get mode() {
|
|
78
|
-
return this._mode;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private _mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
|
|
85
|
+
@provide({ context: efContext })
|
|
86
|
+
efContext = this;
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
converter: durationConverter,
|
|
86
|
-
attribute: "overlap",
|
|
87
|
-
})
|
|
88
|
-
set overlapMs(value: number) {
|
|
89
|
-
// Invalidate duration cache when overlap changes
|
|
90
|
-
sequenceDurationCache.delete(this);
|
|
91
|
-
this._overlapMs = value;
|
|
92
|
-
}
|
|
88
|
+
mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
|
|
89
|
+
overlapMs = 0;
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
attributeChangedCallback(
|
|
92
|
+
name: string,
|
|
93
|
+
old: string | null,
|
|
94
|
+
value: string | null,
|
|
95
|
+
): void {
|
|
96
|
+
if (name === "mode" && value) {
|
|
97
|
+
this.mode = value as typeof this.mode;
|
|
98
|
+
}
|
|
99
|
+
if (name === "overlap" && value) {
|
|
100
|
+
this.overlapMs = parseTimeToMs(value);
|
|
101
|
+
}
|
|
102
|
+
super.attributeChangedCallback(name, old, value);
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
private _overlapMs = 0;
|
|
99
|
-
|
|
100
105
|
@property({ type: String })
|
|
101
106
|
fit: "none" | "contain" | "cover" = "none";
|
|
102
107
|
|
|
103
108
|
#resizeObserver?: ResizeObserver;
|
|
104
109
|
|
|
110
|
+
#currentTime: number | undefined = undefined;
|
|
105
111
|
#seekInProgress = false;
|
|
106
|
-
|
|
107
112
|
#pendingSeekTime: number | undefined;
|
|
108
|
-
|
|
109
113
|
#processingPendingSeek = false;
|
|
110
114
|
|
|
111
|
-
#frameTaskInProgress = false;
|
|
112
|
-
|
|
113
|
-
#pendingFrameTaskRun = false;
|
|
114
|
-
|
|
115
|
-
#processingPendingFrameTask = false;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Throttles frameTask execution to ensure only one runs at a time while preserving the last request
|
|
119
|
-
*/
|
|
120
115
|
private async runThrottledFrameTask(): Promise<void> {
|
|
121
|
-
if (this
|
|
122
|
-
this
|
|
123
|
-
// Wait for the current frame task to complete
|
|
124
|
-
while (this.#frameTaskInProgress) {
|
|
125
|
-
await this.frameTask.taskComplete;
|
|
126
|
-
}
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.#frameTaskInProgress = true;
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await this.frameTask.run();
|
|
134
|
-
} finally {
|
|
135
|
-
this.#frameTaskInProgress = false;
|
|
136
|
-
|
|
137
|
-
if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
|
|
138
|
-
this.#pendingFrameTaskRun = false;
|
|
139
|
-
this.#processingPendingFrameTask = true;
|
|
140
|
-
try {
|
|
141
|
-
await this.runThrottledFrameTask();
|
|
142
|
-
} finally {
|
|
143
|
-
this.#processingPendingFrameTask = false;
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
this.#pendingFrameTaskRun = false;
|
|
147
|
-
}
|
|
116
|
+
if (this.playbackController) {
|
|
117
|
+
return this.playbackController.runThrottledFrameTask();
|
|
148
118
|
}
|
|
119
|
+
await this.frameTask.run();
|
|
149
120
|
}
|
|
150
121
|
|
|
151
122
|
@property({ type: Number, attribute: "currenttime" })
|
|
152
123
|
set currentTime(time: number) {
|
|
153
|
-
|
|
124
|
+
if (this.playbackController) {
|
|
125
|
+
this.playbackController.currentTime = time;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
time = Math.max(0, Math.min(this.durationMs / 1000, time));
|
|
154
130
|
if (!this.isRootTimegroup) {
|
|
155
131
|
return;
|
|
156
132
|
}
|
|
@@ -193,6 +169,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
193
169
|
}
|
|
194
170
|
|
|
195
171
|
get currentTime() {
|
|
172
|
+
if (this.playbackController) {
|
|
173
|
+
return this.playbackController.currentTime;
|
|
174
|
+
}
|
|
196
175
|
return this.#currentTime ?? 0;
|
|
197
176
|
}
|
|
198
177
|
|
|
@@ -204,6 +183,49 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
204
183
|
return this.currentTime * 1000;
|
|
205
184
|
}
|
|
206
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Seek to a specific time and wait for all frames to be ready.
|
|
188
|
+
* This is the recommended way to seek in tests and programmatic control.
|
|
189
|
+
*
|
|
190
|
+
* @param timeMs - Time in milliseconds to seek to
|
|
191
|
+
* @returns Promise that resolves when the seek is complete and all visible children are ready
|
|
192
|
+
*/
|
|
193
|
+
async seek(timeMs: number): Promise<void> {
|
|
194
|
+
this.currentTimeMs = timeMs;
|
|
195
|
+
await this.seekTask.taskComplete;
|
|
196
|
+
|
|
197
|
+
// Handle localStorage when playbackController delegates seek
|
|
198
|
+
if (this.playbackController) {
|
|
199
|
+
this.saveTimeToLocalStorage(this.currentTime);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await this.frameTask.taskComplete;
|
|
203
|
+
|
|
204
|
+
// Ensure all visible elements have completed their reactive update cycles AND frame rendering
|
|
205
|
+
// waitForFrameTasks() calls frameTask.run() on children, but this may happen before child
|
|
206
|
+
// elements have processed property changes from requestUpdate(). To ensure frame data is
|
|
207
|
+
// accurate, we wait for updateComplete first, then ensure the frameTask has run with the
|
|
208
|
+
// updated properties. Elements like EFVideo provide waitForFrameReady() for this pattern.
|
|
209
|
+
const temporalElements = deepGetElementsWithFrameTasks(this);
|
|
210
|
+
const visibleElements = temporalElements.filter((element) => {
|
|
211
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
212
|
+
return animationState.isVisible;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await Promise.all(
|
|
216
|
+
visibleElements.map(async (element) => {
|
|
217
|
+
if (
|
|
218
|
+
"waitForFrameReady" in element &&
|
|
219
|
+
typeof element.waitForFrameReady === "function"
|
|
220
|
+
) {
|
|
221
|
+
await (element as any).waitForFrameReady();
|
|
222
|
+
} else {
|
|
223
|
+
await element.updateComplete;
|
|
224
|
+
}
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
207
229
|
/**
|
|
208
230
|
* Determines if this is a root timegroup (no parent timegroups)
|
|
209
231
|
*/
|
|
@@ -211,10 +233,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
211
233
|
return !this.parentTimegroup;
|
|
212
234
|
}
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
* Saves time to localStorage (extracted for reuse)
|
|
216
|
-
*/
|
|
217
|
-
#saveTimeToLocalStorage(time: number) {
|
|
236
|
+
saveTimeToLocalStorage(time: number) {
|
|
218
237
|
try {
|
|
219
238
|
if (this.id && this.isConnected && !Number.isNaN(time)) {
|
|
220
239
|
localStorage.setItem(this.storageKey, time.toString());
|
|
@@ -238,7 +257,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
238
257
|
this.requestUpdate();
|
|
239
258
|
};
|
|
240
259
|
|
|
241
|
-
|
|
260
|
+
loadTimeFromLocalStorage(): number | undefined {
|
|
242
261
|
if (this.id) {
|
|
243
262
|
try {
|
|
244
263
|
const storedValue = localStorage.getItem(this.storageKey);
|
|
@@ -250,21 +269,25 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
250
269
|
log("Failed to load time from localStorage", error);
|
|
251
270
|
}
|
|
252
271
|
}
|
|
272
|
+
return undefined;
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
connectedCallback() {
|
|
256
276
|
super.connectedCallback();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
277
|
+
|
|
278
|
+
if (!this.playbackController) {
|
|
279
|
+
this.waitForMediaDurations().then(() => {
|
|
280
|
+
if (this.id) {
|
|
281
|
+
const maybeLoadedTime = this.loadTimeFromLocalStorage();
|
|
282
|
+
if (maybeLoadedTime !== undefined) {
|
|
283
|
+
this.currentTime = maybeLoadedTime;
|
|
284
|
+
}
|
|
262
285
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
286
|
+
if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
|
|
287
|
+
this.seekTask.run();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
268
291
|
|
|
269
292
|
if (this.parentTimegroup) {
|
|
270
293
|
new TimegroupController(this.parentTimegroup, this);
|
|
@@ -277,7 +300,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
277
300
|
|
|
278
301
|
#previousDurationMs = 0;
|
|
279
302
|
|
|
280
|
-
protected updated(
|
|
303
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
304
|
+
super.updated(changedProperties);
|
|
305
|
+
|
|
306
|
+
if (changedProperties.has("mode") || changedProperties.has("overlapMs")) {
|
|
307
|
+
sequenceDurationCache.delete(this);
|
|
308
|
+
}
|
|
309
|
+
|
|
281
310
|
if (this.#previousDurationMs !== this.durationMs) {
|
|
282
311
|
this.#previousDurationMs = this.durationMs;
|
|
283
312
|
this.runThrottledFrameTask();
|
|
@@ -422,18 +451,47 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
422
451
|
}
|
|
423
452
|
|
|
424
453
|
async waitForFrameTasks() {
|
|
425
|
-
const
|
|
454
|
+
const result = await withSpan(
|
|
455
|
+
"timegroup.waitForFrameTasks",
|
|
456
|
+
{
|
|
457
|
+
timegroupId: this.id || "unknown",
|
|
458
|
+
mode: this.mode,
|
|
459
|
+
},
|
|
460
|
+
undefined,
|
|
461
|
+
async (span) => {
|
|
462
|
+
const innerStart = performance.now();
|
|
463
|
+
|
|
464
|
+
const temporalElements = deepGetElementsWithFrameTasks(this);
|
|
465
|
+
if (isTracingEnabled()) {
|
|
466
|
+
span.setAttribute("temporalElementsCount", temporalElements.length);
|
|
467
|
+
}
|
|
426
468
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
469
|
+
// Filter to only include temporally visible elements for frame processing
|
|
470
|
+
// Use animation-friendly visibility to prevent animation jumps at exact boundaries
|
|
471
|
+
const visibleElements = temporalElements.filter((element) => {
|
|
472
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
473
|
+
return animationState.isVisible;
|
|
474
|
+
});
|
|
475
|
+
if (isTracingEnabled()) {
|
|
476
|
+
span.setAttribute("visibleElementsCount", visibleElements.length);
|
|
477
|
+
}
|
|
433
478
|
|
|
434
|
-
|
|
435
|
-
|
|
479
|
+
const promiseStart = performance.now();
|
|
480
|
+
|
|
481
|
+
await Promise.all(
|
|
482
|
+
visibleElements.map((element) => element.frameTask.run()),
|
|
483
|
+
);
|
|
484
|
+
const promiseEnd = performance.now();
|
|
485
|
+
|
|
486
|
+
const innerEnd = performance.now();
|
|
487
|
+
if (isTracingEnabled()) {
|
|
488
|
+
span.setAttribute("actualInnerMs", innerEnd - innerStart);
|
|
489
|
+
span.setAttribute("promiseAwaitMs", promiseEnd - promiseStart);
|
|
490
|
+
}
|
|
491
|
+
},
|
|
436
492
|
);
|
|
493
|
+
|
|
494
|
+
return result;
|
|
437
495
|
}
|
|
438
496
|
|
|
439
497
|
mediaDurationsPromise: Promise<void> | undefined = undefined;
|
|
@@ -452,37 +510,53 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
452
510
|
* in calculations and it was not clear why.
|
|
453
511
|
*/
|
|
454
512
|
async #waitForMediaDurations() {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
513
|
+
return withSpan(
|
|
514
|
+
"timegroup.waitForMediaDurations",
|
|
515
|
+
{
|
|
516
|
+
timegroupId: this.id || "unknown",
|
|
517
|
+
mode: this.mode,
|
|
518
|
+
},
|
|
519
|
+
undefined,
|
|
520
|
+
async (span) => {
|
|
521
|
+
// We must await updateComplete to ensure all media elements inside this are connected
|
|
522
|
+
// and will match deepGetMediaElements
|
|
523
|
+
await this.updateComplete;
|
|
524
|
+
const mediaElements = deepGetMediaElements(this);
|
|
525
|
+
if (isTracingEnabled()) {
|
|
526
|
+
span.setAttribute("mediaElementsCount", mediaElements.length);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Then, we must await the fragmentIndexTask to ensure all media elements have their
|
|
530
|
+
// fragment index loaded, which is where their duration is parsed from.
|
|
531
|
+
await Promise.all(
|
|
532
|
+
mediaElements.map((m) =>
|
|
533
|
+
m.mediaEngineTask.value
|
|
534
|
+
? Promise.resolve()
|
|
535
|
+
: m.mediaEngineTask.run(),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
466
538
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
539
|
+
// After waiting for durations, we must force some updates to cascade and ensure all temporal elements
|
|
540
|
+
// have correct durations and start times. It is not ideal that we have to do this inside here,
|
|
541
|
+
// but it is the best current way to ensure that all temporal elements have correct durations and start times.
|
|
470
542
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
543
|
+
// Next, we must flush the startTimeMs cache to ensure all media elements have their
|
|
544
|
+
// startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
|
|
545
|
+
flushStartTimeMsCache();
|
|
474
546
|
|
|
475
|
-
|
|
476
|
-
|
|
547
|
+
// Flush duration cache since child durations may have changed
|
|
548
|
+
flushSequenceDurationCache();
|
|
477
549
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
550
|
+
// Request an update to the currentTime of this group, ensuring that time updates will cascade
|
|
551
|
+
// down to children, forcing sequence groups to arrange correctly.
|
|
552
|
+
// This also makes the filmstrip update correctly.
|
|
553
|
+
this.requestUpdate("currentTime");
|
|
554
|
+
// Finally, we must await updateComplete to ensure all temporal elements have their
|
|
555
|
+
// currentTime updated and all animations have run.
|
|
484
556
|
|
|
485
|
-
|
|
557
|
+
await this.updateComplete;
|
|
558
|
+
},
|
|
559
|
+
);
|
|
486
560
|
}
|
|
487
561
|
|
|
488
562
|
get childTemporals() {
|
|
@@ -503,13 +577,31 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
503
577
|
/**
|
|
504
578
|
* Returns true if the timegroup should be wrapped with a workbench.
|
|
505
579
|
*
|
|
506
|
-
* A timegroup should be wrapped with a workbench if
|
|
507
|
-
*
|
|
580
|
+
* A timegroup should be wrapped with a workbench if:
|
|
581
|
+
* - It's being rendered (EF_RENDERING), OR
|
|
582
|
+
* - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
|
|
508
583
|
*
|
|
509
|
-
* If the timegroup is already
|
|
584
|
+
* If the timegroup is already wrapped in a context provider like ef-preview,
|
|
510
585
|
* it should NOT be wrapped in a workbench.
|
|
511
586
|
*/
|
|
512
587
|
shouldWrapWithWorkbench() {
|
|
588
|
+
const isRendering = EF_RENDERING?.() === true;
|
|
589
|
+
|
|
590
|
+
// During rendering, always wrap with workbench (needed by EF_FRAMEGEN)
|
|
591
|
+
if (isRendering) {
|
|
592
|
+
return (
|
|
593
|
+
this.closest("ef-timegroup") === this &&
|
|
594
|
+
this.closest("ef-preview") === null &&
|
|
595
|
+
this.closest("ef-workbench") === null &&
|
|
596
|
+
this.closest("test-context") === null
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// During interactive mode, respect the dev workbench flag
|
|
601
|
+
if (!globalThis.EF_DEV_WORKBENCH) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
513
605
|
return (
|
|
514
606
|
EF_INTERACTIVE &&
|
|
515
607
|
this.closest("ef-timegroup") === this &&
|
|
@@ -542,138 +634,22 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
542
634
|
);
|
|
543
635
|
}
|
|
544
636
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Create AbortController for audio fetch operations
|
|
553
|
-
const abortController = new AbortController();
|
|
554
|
-
|
|
555
|
-
await Promise.all(
|
|
556
|
-
deepGetMediaElements(this).map(async (mediaElement) => {
|
|
557
|
-
// Skip muted elements entirely - no audio fetching or processing needed
|
|
558
|
-
if (mediaElement.mute) {
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
|
|
563
|
-
const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
|
|
564
|
-
const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
|
|
565
|
-
if (!mediaOverlaps) {
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Convert from root timegroup timeline to media element's local timeline
|
|
570
|
-
const mediaLocalFromMs = Math.max(0, fromMs - mediaElement.startTimeMs);
|
|
571
|
-
const mediaLocalToMs = Math.min(
|
|
572
|
-
mediaElement.endTimeMs - mediaElement.startTimeMs,
|
|
573
|
-
toMs - mediaElement.startTimeMs,
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
// Skip if no valid local time range
|
|
577
|
-
if (mediaLocalFromMs >= mediaLocalToMs) {
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Convert from local timeline to source media timeline (accounting for sourcein/sourceout)
|
|
582
|
-
const sourceInMs =
|
|
583
|
-
mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
|
|
584
|
-
const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
|
|
585
|
-
const mediaSourceToMs = mediaLocalToMs + sourceInMs;
|
|
586
|
-
|
|
587
|
-
let audio: AudioSpan | undefined;
|
|
588
|
-
try {
|
|
589
|
-
audio = await mediaElement.fetchAudioSpanningTime(
|
|
590
|
-
mediaSourceFromMs,
|
|
591
|
-
mediaSourceToMs,
|
|
592
|
-
abortController.signal,
|
|
593
|
-
);
|
|
594
|
-
} catch (error) {
|
|
595
|
-
if (
|
|
596
|
-
error instanceof Error &&
|
|
597
|
-
error.message.includes("No audio track available")
|
|
598
|
-
) {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
throw error;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (!audio) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const bufferSource = audioContext.createBufferSource();
|
|
609
|
-
bufferSource.buffer = await audioContext.decodeAudioData(
|
|
610
|
-
await audio.blob.arrayBuffer(),
|
|
611
|
-
);
|
|
612
|
-
bufferSource.connect(audioContext.destination);
|
|
613
|
-
|
|
614
|
-
// Calculate timing for placing this audio in the output context
|
|
615
|
-
const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
|
|
616
|
-
|
|
617
|
-
// Calculate offset within the fetched audio buffer
|
|
618
|
-
// audio.startMs is now in source timeline, convert back to compare properly
|
|
619
|
-
const requestedSourceFromMs = mediaSourceFromMs;
|
|
620
|
-
const actualSourceStartMs = audio.startMs;
|
|
621
|
-
const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
|
|
622
|
-
|
|
623
|
-
// Ensure offset is never negative (this would cause audio scheduling errors)
|
|
624
|
-
const safeOffsetMs = Math.max(0, offsetInBufferMs);
|
|
625
|
-
|
|
626
|
-
// Calculate exact duration to play from the buffer (don't exceed what we need)
|
|
627
|
-
const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
|
|
628
|
-
const availableAudioMs = audio.endMs - audio.startMs;
|
|
629
|
-
const actualDurationMs = Math.min(
|
|
630
|
-
requestedDurationMs,
|
|
631
|
-
availableAudioMs - safeOffsetMs,
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
if (actualDurationMs <= 0) {
|
|
635
|
-
return; // Skip if no valid audio duration
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
bufferSource.start(
|
|
639
|
-
ctxStartMs / 1000, // When to start in output context (seconds)
|
|
640
|
-
safeOffsetMs / 1000, // Offset into the fetched buffer (seconds)
|
|
641
|
-
actualDurationMs / 1000, // How long to play from buffer (seconds)
|
|
642
|
-
);
|
|
643
|
-
}),
|
|
644
|
-
);
|
|
637
|
+
/**
|
|
638
|
+
* Returns media elements for playback audio rendering
|
|
639
|
+
* For standalone media, returns [this]; for timegroups, returns all descendants
|
|
640
|
+
* Used by PlaybackController for audio-driven playback
|
|
641
|
+
*/
|
|
642
|
+
getMediaElements(): EFMedia[] {
|
|
643
|
+
return deepGetMediaElements(this);
|
|
645
644
|
}
|
|
646
645
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const alignedFrames = Math.round(aacFrames);
|
|
655
|
-
const contextSize = alignedFrames * 1024; // AAC-aligned sample count
|
|
656
|
-
|
|
657
|
-
// Debug logging for audio duration calculations
|
|
658
|
-
if (contextSize <= 0) {
|
|
659
|
-
throw new Error(
|
|
660
|
-
`Duration must be greater than 0 when rendering audio. ${contextSize}ms`,
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
let audioContext: OfflineAudioContext;
|
|
665
|
-
try {
|
|
666
|
-
audioContext = new OfflineAudioContext(2, contextSize, 48000);
|
|
667
|
-
} catch (error) {
|
|
668
|
-
throw new Error(
|
|
669
|
-
`[EFTimegroup.renderAudio] Failed to create OfflineAudioContext(2, ${contextSize}, 48000) for renderAudio(${fromMs}, ${toMs}) with contextSize=${contextSize}: ${error instanceof Error ? error.message : String(error)}. This typically happens when audio parameters are invalid (e.g., contextSize <= 0).`,
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
await this.#addAudioToContext(audioContext, fromMs, toMs);
|
|
674
|
-
const renderedBuffer = await audioContext.startRendering();
|
|
675
|
-
|
|
676
|
-
return renderedBuffer;
|
|
646
|
+
/**
|
|
647
|
+
* Render audio buffer for playback
|
|
648
|
+
* Called by PlaybackController during live playback
|
|
649
|
+
* Delegates to shared renderTemporalAudio utility for consistent behavior
|
|
650
|
+
*/
|
|
651
|
+
async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {
|
|
652
|
+
return renderTemporalAudio(this, fromMs, toMs);
|
|
677
653
|
}
|
|
678
654
|
|
|
679
655
|
/**
|
|
@@ -717,7 +693,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
717
693
|
|
|
718
694
|
await Promise.all(loaderTasks);
|
|
719
695
|
|
|
720
|
-
efElements.
|
|
696
|
+
efElements.forEach((el) => {
|
|
721
697
|
if ("productionSrc" in el && el.productionSrc instanceof Function) {
|
|
722
698
|
el.setAttribute("src", el.productionSrc());
|
|
723
699
|
}
|
|
@@ -728,10 +704,21 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
728
704
|
// autoRun: EF_INTERACTIVE,
|
|
729
705
|
autoRun: false,
|
|
730
706
|
args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
|
|
731
|
-
task: async ([]) => {
|
|
707
|
+
task: async ([ownCurrentTimeMs, currentTimeMs]) => {
|
|
732
708
|
if (this.isRootTimegroup) {
|
|
733
|
-
await
|
|
734
|
-
|
|
709
|
+
await withSpan(
|
|
710
|
+
"timegroup.frameTask",
|
|
711
|
+
{
|
|
712
|
+
timegroupId: this.id || "unknown",
|
|
713
|
+
ownCurrentTimeMs,
|
|
714
|
+
currentTimeMs,
|
|
715
|
+
},
|
|
716
|
+
undefined,
|
|
717
|
+
async () => {
|
|
718
|
+
await this.waitForFrameTasks();
|
|
719
|
+
updateAnimations(this);
|
|
720
|
+
},
|
|
721
|
+
);
|
|
735
722
|
}
|
|
736
723
|
},
|
|
737
724
|
});
|
|
@@ -741,22 +728,41 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
741
728
|
args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
|
|
742
729
|
onComplete: () => {},
|
|
743
730
|
task: async ([targetTime]) => {
|
|
731
|
+
if (this.playbackController) {
|
|
732
|
+
await this.playbackController.seekTask.taskComplete;
|
|
733
|
+
return this.currentTime;
|
|
734
|
+
}
|
|
735
|
+
|
|
744
736
|
if (!this.isRootTimegroup) {
|
|
745
737
|
return;
|
|
746
738
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
739
|
+
return withSpan(
|
|
740
|
+
"timegroup.seekTask",
|
|
741
|
+
{
|
|
742
|
+
timegroupId: this.id || "unknown",
|
|
743
|
+
targetTime: targetTime ?? 0,
|
|
744
|
+
durationMs: this.durationMs,
|
|
745
|
+
},
|
|
746
|
+
undefined,
|
|
747
|
+
async (span) => {
|
|
748
|
+
await this.waitForMediaDurations();
|
|
749
|
+
const newTime = Math.max(
|
|
750
|
+
0,
|
|
751
|
+
Math.min(targetTime ?? 0, this.durationMs / 1000),
|
|
752
|
+
);
|
|
753
|
+
if (isTracingEnabled()) {
|
|
754
|
+
span.setAttribute("newTime", newTime);
|
|
755
|
+
}
|
|
756
|
+
// Apply the clamped time back to currentTime
|
|
757
|
+
|
|
758
|
+
this.#currentTime = newTime;
|
|
759
|
+
this.requestUpdate("currentTime");
|
|
760
|
+
await this.runThrottledFrameTask();
|
|
761
|
+
this.saveTimeToLocalStorage(this.#currentTime);
|
|
762
|
+
this.#seekInProgress = false;
|
|
763
|
+
return newTime;
|
|
764
|
+
},
|
|
751
765
|
);
|
|
752
|
-
// Apply the clamped time back to currentTime
|
|
753
|
-
this.#currentTime = newTime;
|
|
754
|
-
this.requestUpdate("currentTime");
|
|
755
|
-
await this.runThrottledFrameTask();
|
|
756
|
-
this.#saveTimeToLocalStorage(this.#currentTime);
|
|
757
|
-
// This has to be set false here so any following seeks are not treated as pending
|
|
758
|
-
this.#seekInProgress = false;
|
|
759
|
-
return newTime;
|
|
760
766
|
},
|
|
761
767
|
});
|
|
762
768
|
}
|