@editframe/elements 0.21.0-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/EF_FRAMEGEN.js +2 -3
- 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 +2 -2
- package/dist/elements/EFCaptions.d.ts +1 -3
- package/dist/elements/EFCaptions.js +59 -51
- package/dist/elements/EFImage.js +2 -2
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +1 -2
- package/dist/elements/EFMedia/AssetMediaEngine.js +1 -3
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +2 -4
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +4 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -2
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +5 -9
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -3
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +4 -7
- package/dist/elements/EFMedia.d.ts +19 -0
- package/dist/elements/EFMedia.js +19 -2
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSurface.js +1 -1
- package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
- package/dist/elements/EFTemporal.d.ts +10 -0
- package/dist/elements/EFTemporal.js +82 -5
- package/dist/elements/EFThumbnailStrip.js +9 -16
- package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
- package/dist/elements/EFTimegroup.d.ts +35 -14
- package/dist/elements/EFTimegroup.js +72 -120
- package/dist/elements/EFVideo.d.ts +10 -0
- package/dist/elements/EFVideo.js +15 -2
- package/dist/elements/EFWaveform.js +10 -18
- package/dist/elements/SampleBuffer.js +1 -2
- package/dist/elements/TargetController.js +2 -2
- package/dist/elements/renderTemporalAudio.d.ts +10 -0
- package/dist/elements/renderTemporalAudio.js +35 -0
- package/dist/elements/updateAnimations.js +7 -10
- package/dist/gui/ContextMixin.d.ts +5 -5
- package/dist/gui/ContextMixin.js +151 -117
- 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 +1 -1
- package/dist/gui/EFControls.browsertest.d.ts +11 -0
- package/dist/gui/EFControls.d.ts +18 -4
- package/dist/gui/EFControls.js +67 -25
- 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 +140 -34
- package/dist/gui/EFFitScale.js +2 -4
- package/dist/gui/EFFocusOverlay.js +1 -1
- 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 +15 -6
- 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 +7 -7
- package/dist/gui/EFTimeDisplay.d.ts +7 -1
- package/dist/gui/EFTimeDisplay.js +5 -5
- package/dist/gui/EFToggleLoop.d.ts +9 -3
- package/dist/gui/EFToggleLoop.js +6 -4
- package/dist/gui/EFTogglePlay.d.ts +12 -4
- package/dist/gui/EFTogglePlay.js +24 -19
- package/dist/gui/EFWorkbench.js +1 -1
- 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/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 +4 -0
- package/dist/index.js +5 -1
- package/dist/otel/setupBrowserTracing.d.ts +1 -1
- package/dist/otel/setupBrowserTracing.js +6 -4
- package/dist/otel/tracingHelpers.js +1 -2
- package/dist/style.css +1 -1
- package/package.json +5 -5
- package/src/elements/ContextProxiesController.ts +10 -10
- package/src/elements/EFAudio.ts +1 -0
- package/src/elements/EFCaptions.browsertest.ts +128 -58
- package/src/elements/EFCaptions.ts +60 -34
- package/src/elements/EFImage.browsertest.ts +1 -2
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
- package/src/elements/EFMedia.browsertest.ts +8 -15
- package/src/elements/EFMedia.ts +38 -7
- 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 +6 -7
- package/src/elements/EFTimegroup.ts +162 -244
- package/src/elements/EFVideo.browsertest.ts +143 -47
- package/src/elements/EFVideo.ts +26 -0
- 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/setupBrowserTracing.ts +17 -12
- package/test/cache-integration-verification.browsertest.ts +1 -1
- package/types.json +1 -1
- package/dist/elements/ContextProxiesController.js +0 -49
- /package/dist/_virtual/{_@oxc-project_runtime@0.93.0 → _@oxc-project_runtime@0.94.0}/helpers/decorate.js +0 -0
|
@@ -5,11 +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";
|
|
10
|
+
import { efContext } from "../gui/efContext.js";
|
|
11
|
+
import { TWMixin } from "../gui/TWMixin.js";
|
|
9
12
|
import { isTracingEnabled, withSpan } from "../otel/tracingHelpers.js";
|
|
10
|
-
import type
|
|
11
|
-
import { durationConverter } from "./durationConverter.js";
|
|
12
|
-
import { deepGetMediaElements } from "./EFMedia.js";
|
|
13
|
+
import { deepGetMediaElements, type EFMedia } from "./EFMedia.js";
|
|
13
14
|
import {
|
|
14
15
|
deepGetElementsWithFrameTasks,
|
|
15
16
|
EFTemporal,
|
|
@@ -18,12 +19,19 @@ import {
|
|
|
18
19
|
shallowGetTemporalElements,
|
|
19
20
|
timegroupContext,
|
|
20
21
|
} from "./EFTemporal.js";
|
|
22
|
+
import { parseTimeToMs } from "./parseTimeToMs.js";
|
|
23
|
+
import { renderTemporalAudio } from "./renderTemporalAudio.js";
|
|
24
|
+
import { EFTargetable } from "./TargetController.js";
|
|
21
25
|
import { TimegroupController } from "./TimegroupController.js";
|
|
22
26
|
import {
|
|
23
27
|
evaluateTemporalStateForAnimation,
|
|
24
28
|
updateAnimations,
|
|
25
29
|
} from "./updateAnimations.ts";
|
|
26
30
|
|
|
31
|
+
declare global {
|
|
32
|
+
var EF_DEV_WORKBENCH: boolean | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
const log = debug("ef:elements:EFTimegroup");
|
|
28
36
|
|
|
29
37
|
// Cache for sequence mode duration calculations to avoid O(n) recalculation
|
|
@@ -48,13 +56,24 @@ export const shallowGetTimegroups = (
|
|
|
48
56
|
};
|
|
49
57
|
|
|
50
58
|
@customElement("ef-timegroup")
|
|
51
|
-
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
|
+
|
|
52
66
|
static styles = css`
|
|
53
67
|
:host {
|
|
54
68
|
display: block;
|
|
69
|
+
position: relative;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
::slotted(ef-timegroup) {
|
|
74
|
+
position: absolute;
|
|
55
75
|
width: 100%;
|
|
56
76
|
height: 100%;
|
|
57
|
-
position: absolute;
|
|
58
77
|
top: 0;
|
|
59
78
|
left: 0;
|
|
60
79
|
}
|
|
@@ -63,95 +82,51 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
63
82
|
@provide({ context: timegroupContext })
|
|
64
83
|
_timeGroupContext = this;
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@property({
|
|
69
|
-
type: String,
|
|
70
|
-
attribute: "mode",
|
|
71
|
-
})
|
|
72
|
-
set mode(value: "fit" | "fixed" | "sequence" | "contain") {
|
|
73
|
-
// Invalidate duration cache when mode changes
|
|
74
|
-
sequenceDurationCache.delete(this);
|
|
75
|
-
this._mode = value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
get mode() {
|
|
79
|
-
return this._mode;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private _mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
|
|
85
|
+
@provide({ context: efContext })
|
|
86
|
+
efContext = this;
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
converter: durationConverter,
|
|
87
|
-
attribute: "overlap",
|
|
88
|
-
})
|
|
89
|
-
set overlapMs(value: number) {
|
|
90
|
-
// Invalidate duration cache when overlap changes
|
|
91
|
-
sequenceDurationCache.delete(this);
|
|
92
|
-
this._overlapMs = value;
|
|
93
|
-
}
|
|
88
|
+
mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
|
|
89
|
+
overlapMs = 0;
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
private _overlapMs = 0;
|
|
100
|
-
|
|
101
105
|
@property({ type: String })
|
|
102
106
|
fit: "none" | "contain" | "cover" = "none";
|
|
103
107
|
|
|
104
108
|
#resizeObserver?: ResizeObserver;
|
|
105
109
|
|
|
110
|
+
#currentTime: number | undefined = undefined;
|
|
106
111
|
#seekInProgress = false;
|
|
107
|
-
|
|
108
112
|
#pendingSeekTime: number | undefined;
|
|
109
|
-
|
|
110
113
|
#processingPendingSeek = false;
|
|
111
114
|
|
|
112
|
-
#frameTaskInProgress = false;
|
|
113
|
-
|
|
114
|
-
#pendingFrameTaskRun = false;
|
|
115
|
-
|
|
116
|
-
#processingPendingFrameTask = false;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Throttles frameTask execution to ensure only one runs at a time while preserving the last request
|
|
120
|
-
*/
|
|
121
115
|
private async runThrottledFrameTask(): Promise<void> {
|
|
122
|
-
if (this
|
|
123
|
-
this
|
|
124
|
-
// Wait for the current frame task to complete
|
|
125
|
-
while (this.#frameTaskInProgress) {
|
|
126
|
-
await this.frameTask.taskComplete;
|
|
127
|
-
}
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
this.#frameTaskInProgress = true;
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await this.frameTask.run();
|
|
135
|
-
} finally {
|
|
136
|
-
this.#frameTaskInProgress = false;
|
|
137
|
-
|
|
138
|
-
if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
|
|
139
|
-
this.#pendingFrameTaskRun = false;
|
|
140
|
-
this.#processingPendingFrameTask = true;
|
|
141
|
-
try {
|
|
142
|
-
await this.runThrottledFrameTask();
|
|
143
|
-
} finally {
|
|
144
|
-
this.#processingPendingFrameTask = false;
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
this.#pendingFrameTaskRun = false;
|
|
148
|
-
}
|
|
116
|
+
if (this.playbackController) {
|
|
117
|
+
return this.playbackController.runThrottledFrameTask();
|
|
149
118
|
}
|
|
119
|
+
await this.frameTask.run();
|
|
150
120
|
}
|
|
151
121
|
|
|
152
122
|
@property({ type: Number, attribute: "currenttime" })
|
|
153
123
|
set currentTime(time: number) {
|
|
154
|
-
|
|
124
|
+
if (this.playbackController) {
|
|
125
|
+
this.playbackController.currentTime = time;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
time = Math.max(0, Math.min(this.durationMs / 1000, time));
|
|
155
130
|
if (!this.isRootTimegroup) {
|
|
156
131
|
return;
|
|
157
132
|
}
|
|
@@ -194,6 +169,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
194
169
|
}
|
|
195
170
|
|
|
196
171
|
get currentTime() {
|
|
172
|
+
if (this.playbackController) {
|
|
173
|
+
return this.playbackController.currentTime;
|
|
174
|
+
}
|
|
197
175
|
return this.#currentTime ?? 0;
|
|
198
176
|
}
|
|
199
177
|
|
|
@@ -205,6 +183,49 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
205
183
|
return this.currentTime * 1000;
|
|
206
184
|
}
|
|
207
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
|
+
|
|
208
229
|
/**
|
|
209
230
|
* Determines if this is a root timegroup (no parent timegroups)
|
|
210
231
|
*/
|
|
@@ -212,10 +233,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
212
233
|
return !this.parentTimegroup;
|
|
213
234
|
}
|
|
214
235
|
|
|
215
|
-
|
|
216
|
-
* Saves time to localStorage (extracted for reuse)
|
|
217
|
-
*/
|
|
218
|
-
#saveTimeToLocalStorage(time: number) {
|
|
236
|
+
saveTimeToLocalStorage(time: number) {
|
|
219
237
|
try {
|
|
220
238
|
if (this.id && this.isConnected && !Number.isNaN(time)) {
|
|
221
239
|
localStorage.setItem(this.storageKey, time.toString());
|
|
@@ -239,7 +257,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
239
257
|
this.requestUpdate();
|
|
240
258
|
};
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
loadTimeFromLocalStorage(): number | undefined {
|
|
243
261
|
if (this.id) {
|
|
244
262
|
try {
|
|
245
263
|
const storedValue = localStorage.getItem(this.storageKey);
|
|
@@ -251,21 +269,25 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
251
269
|
log("Failed to load time from localStorage", error);
|
|
252
270
|
}
|
|
253
271
|
}
|
|
272
|
+
return undefined;
|
|
254
273
|
}
|
|
255
274
|
|
|
256
275
|
connectedCallback() {
|
|
257
276
|
super.connectedCallback();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
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
|
+
}
|
|
263
285
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}
|
|
286
|
+
if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
|
|
287
|
+
this.seekTask.run();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
269
291
|
|
|
270
292
|
if (this.parentTimegroup) {
|
|
271
293
|
new TimegroupController(this.parentTimegroup, this);
|
|
@@ -278,7 +300,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
278
300
|
|
|
279
301
|
#previousDurationMs = 0;
|
|
280
302
|
|
|
281
|
-
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
|
+
|
|
282
310
|
if (this.#previousDurationMs !== this.durationMs) {
|
|
283
311
|
this.#previousDurationMs = this.durationMs;
|
|
284
312
|
this.runThrottledFrameTask();
|
|
@@ -549,13 +577,31 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
549
577
|
/**
|
|
550
578
|
* Returns true if the timegroup should be wrapped with a workbench.
|
|
551
579
|
*
|
|
552
|
-
* A timegroup should be wrapped with a workbench if
|
|
553
|
-
*
|
|
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
|
|
554
583
|
*
|
|
555
|
-
* If the timegroup is already
|
|
584
|
+
* If the timegroup is already wrapped in a context provider like ef-preview,
|
|
556
585
|
* it should NOT be wrapped in a workbench.
|
|
557
586
|
*/
|
|
558
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
|
+
|
|
559
605
|
return (
|
|
560
606
|
EF_INTERACTIVE &&
|
|
561
607
|
this.closest("ef-timegroup") === this &&
|
|
@@ -588,155 +634,22 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
588
634
|
);
|
|
589
635
|
}
|
|
590
636
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Create AbortController for audio fetch operations
|
|
599
|
-
const abortController = new AbortController();
|
|
600
|
-
|
|
601
|
-
await Promise.all(
|
|
602
|
-
deepGetMediaElements(this).map(async (mediaElement) => {
|
|
603
|
-
// Skip muted elements entirely - no audio fetching or processing needed
|
|
604
|
-
if (mediaElement.mute) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
|
|
609
|
-
const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
|
|
610
|
-
const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
|
|
611
|
-
if (!mediaOverlaps) {
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Convert from root timegroup timeline to media element's local timeline
|
|
616
|
-
const mediaLocalFromMs = Math.max(0, fromMs - mediaElement.startTimeMs);
|
|
617
|
-
const mediaLocalToMs = Math.min(
|
|
618
|
-
mediaElement.endTimeMs - mediaElement.startTimeMs,
|
|
619
|
-
toMs - mediaElement.startTimeMs,
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
// Skip if no valid local time range
|
|
623
|
-
if (mediaLocalFromMs >= mediaLocalToMs) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Convert from local timeline to source media timeline (accounting for sourcein/sourceout)
|
|
628
|
-
const sourceInMs =
|
|
629
|
-
mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
|
|
630
|
-
const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
|
|
631
|
-
const mediaSourceToMs = mediaLocalToMs + sourceInMs;
|
|
632
|
-
|
|
633
|
-
let audio: AudioSpan | undefined;
|
|
634
|
-
try {
|
|
635
|
-
audio = await mediaElement.fetchAudioSpanningTime(
|
|
636
|
-
mediaSourceFromMs,
|
|
637
|
-
mediaSourceToMs,
|
|
638
|
-
abortController.signal,
|
|
639
|
-
);
|
|
640
|
-
} catch (error) {
|
|
641
|
-
if (
|
|
642
|
-
error instanceof Error &&
|
|
643
|
-
error.message.includes("No audio track available")
|
|
644
|
-
) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
throw error;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (!audio) {
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const bufferSource = audioContext.createBufferSource();
|
|
655
|
-
bufferSource.buffer = await audioContext.decodeAudioData(
|
|
656
|
-
await audio.blob.arrayBuffer(),
|
|
657
|
-
);
|
|
658
|
-
bufferSource.connect(audioContext.destination);
|
|
659
|
-
|
|
660
|
-
// Calculate timing for placing this audio in the output context
|
|
661
|
-
const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
|
|
662
|
-
|
|
663
|
-
// Calculate offset within the fetched audio buffer
|
|
664
|
-
// audio.startMs is now in source timeline, convert back to compare properly
|
|
665
|
-
const requestedSourceFromMs = mediaSourceFromMs;
|
|
666
|
-
const actualSourceStartMs = audio.startMs;
|
|
667
|
-
const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
|
|
668
|
-
|
|
669
|
-
// Ensure offset is never negative (this would cause audio scheduling errors)
|
|
670
|
-
const safeOffsetMs = Math.max(0, offsetInBufferMs);
|
|
671
|
-
|
|
672
|
-
// Calculate exact duration to play from the buffer (don't exceed what we need)
|
|
673
|
-
const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
|
|
674
|
-
const availableAudioMs = audio.endMs - audio.startMs;
|
|
675
|
-
const actualDurationMs = Math.min(
|
|
676
|
-
requestedDurationMs,
|
|
677
|
-
availableAudioMs - safeOffsetMs,
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
if (actualDurationMs <= 0) {
|
|
681
|
-
return; // Skip if no valid audio duration
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
bufferSource.start(
|
|
685
|
-
ctxStartMs / 1000, // When to start in output context (seconds)
|
|
686
|
-
safeOffsetMs / 1000, // Offset into the fetched buffer (seconds)
|
|
687
|
-
actualDurationMs / 1000, // How long to play from buffer (seconds)
|
|
688
|
-
);
|
|
689
|
-
}),
|
|
690
|
-
);
|
|
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);
|
|
691
644
|
}
|
|
692
645
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
durationMs: toMs - fromMs,
|
|
701
|
-
},
|
|
702
|
-
undefined,
|
|
703
|
-
async (span) => {
|
|
704
|
-
// Here we determine the number of samples we need to render rather than the duration.
|
|
705
|
-
// We cannot tolerate having more or fewer samples than fit exactlly into AAC frames.
|
|
706
|
-
const durationMs = toMs - fromMs;
|
|
707
|
-
const duration = durationMs / 1000;
|
|
708
|
-
const exactSamples = 48000 * duration;
|
|
709
|
-
const aacFrames = exactSamples / 1024;
|
|
710
|
-
const alignedFrames = Math.round(aacFrames);
|
|
711
|
-
const contextSize = alignedFrames * 1024; // AAC-aligned sample count
|
|
712
|
-
|
|
713
|
-
if (isTracingEnabled()) {
|
|
714
|
-
span.setAttribute("contextSize", contextSize);
|
|
715
|
-
span.setAttribute("alignedFrames", alignedFrames);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Debug logging for audio duration calculations
|
|
719
|
-
if (contextSize <= 0) {
|
|
720
|
-
throw new Error(
|
|
721
|
-
`Duration must be greater than 0 when rendering audio. ${contextSize}ms`,
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
let audioContext: OfflineAudioContext;
|
|
726
|
-
try {
|
|
727
|
-
audioContext = new OfflineAudioContext(2, contextSize, 48000);
|
|
728
|
-
} catch (error) {
|
|
729
|
-
throw new Error(
|
|
730
|
-
`[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).`,
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
await this.#addAudioToContext(audioContext, fromMs, toMs);
|
|
735
|
-
const renderedBuffer = await audioContext.startRendering();
|
|
736
|
-
|
|
737
|
-
return renderedBuffer;
|
|
738
|
-
},
|
|
739
|
-
);
|
|
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);
|
|
740
653
|
}
|
|
741
654
|
|
|
742
655
|
/**
|
|
@@ -780,7 +693,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
780
693
|
|
|
781
694
|
await Promise.all(loaderTasks);
|
|
782
695
|
|
|
783
|
-
efElements.
|
|
696
|
+
efElements.forEach((el) => {
|
|
784
697
|
if ("productionSrc" in el && el.productionSrc instanceof Function) {
|
|
785
698
|
el.setAttribute("src", el.productionSrc());
|
|
786
699
|
}
|
|
@@ -815,6 +728,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
815
728
|
args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
|
|
816
729
|
onComplete: () => {},
|
|
817
730
|
task: async ([targetTime]) => {
|
|
731
|
+
if (this.playbackController) {
|
|
732
|
+
await this.playbackController.seekTask.taskComplete;
|
|
733
|
+
return this.currentTime;
|
|
734
|
+
}
|
|
735
|
+
|
|
818
736
|
if (!this.isRootTimegroup) {
|
|
819
737
|
return;
|
|
820
738
|
}
|
|
@@ -836,11 +754,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
836
754
|
span.setAttribute("newTime", newTime);
|
|
837
755
|
}
|
|
838
756
|
// Apply the clamped time back to currentTime
|
|
757
|
+
|
|
839
758
|
this.#currentTime = newTime;
|
|
840
759
|
this.requestUpdate("currentTime");
|
|
841
760
|
await this.runThrottledFrameTask();
|
|
842
|
-
this
|
|
843
|
-
// This has to be set false here so any following seeks are not treated as pending
|
|
761
|
+
this.saveTimeToLocalStorage(this.#currentTime);
|
|
844
762
|
this.#seekInProgress = false;
|
|
845
763
|
return newTime;
|
|
846
764
|
},
|