@editframe/elements 0.21.0-beta.0 → 0.23.7-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 +73 -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 +163 -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,110 +56,78 @@ 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;
|
|
79
|
+
overflow: initial;
|
|
60
80
|
}
|
|
61
81
|
`;
|
|
62
82
|
|
|
63
83
|
@provide({ context: timegroupContext })
|
|
64
84
|
_timeGroupContext = this;
|
|
65
85
|
|
|
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";
|
|
86
|
+
@provide({ context: efContext })
|
|
87
|
+
efContext = this;
|
|
83
88
|
|
|
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
|
-
}
|
|
89
|
+
mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
|
|
90
|
+
overlapMs = 0;
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
attributeChangedCallback(
|
|
93
|
+
name: string,
|
|
94
|
+
old: string | null,
|
|
95
|
+
value: string | null,
|
|
96
|
+
): void {
|
|
97
|
+
if (name === "mode" && value) {
|
|
98
|
+
this.mode = value as typeof this.mode;
|
|
99
|
+
}
|
|
100
|
+
if (name === "overlap" && value) {
|
|
101
|
+
this.overlapMs = parseTimeToMs(value);
|
|
102
|
+
}
|
|
103
|
+
super.attributeChangedCallback(name, old, value);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
private _overlapMs = 0;
|
|
100
|
-
|
|
101
106
|
@property({ type: String })
|
|
102
107
|
fit: "none" | "contain" | "cover" = "none";
|
|
103
108
|
|
|
104
109
|
#resizeObserver?: ResizeObserver;
|
|
105
110
|
|
|
111
|
+
#currentTime: number | undefined = undefined;
|
|
106
112
|
#seekInProgress = false;
|
|
107
|
-
|
|
108
113
|
#pendingSeekTime: number | undefined;
|
|
109
|
-
|
|
110
114
|
#processingPendingSeek = false;
|
|
111
115
|
|
|
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
116
|
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
|
-
}
|
|
117
|
+
if (this.playbackController) {
|
|
118
|
+
return this.playbackController.runThrottledFrameTask();
|
|
149
119
|
}
|
|
120
|
+
await this.frameTask.run();
|
|
150
121
|
}
|
|
151
122
|
|
|
152
123
|
@property({ type: Number, attribute: "currenttime" })
|
|
153
124
|
set currentTime(time: number) {
|
|
154
|
-
|
|
125
|
+
if (this.playbackController) {
|
|
126
|
+
this.playbackController.currentTime = time;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
time = Math.max(0, Math.min(this.durationMs / 1000, time));
|
|
155
131
|
if (!this.isRootTimegroup) {
|
|
156
132
|
return;
|
|
157
133
|
}
|
|
@@ -194,6 +170,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
194
170
|
}
|
|
195
171
|
|
|
196
172
|
get currentTime() {
|
|
173
|
+
if (this.playbackController) {
|
|
174
|
+
return this.playbackController.currentTime;
|
|
175
|
+
}
|
|
197
176
|
return this.#currentTime ?? 0;
|
|
198
177
|
}
|
|
199
178
|
|
|
@@ -205,6 +184,49 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
205
184
|
return this.currentTime * 1000;
|
|
206
185
|
}
|
|
207
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Seek to a specific time and wait for all frames to be ready.
|
|
189
|
+
* This is the recommended way to seek in tests and programmatic control.
|
|
190
|
+
*
|
|
191
|
+
* @param timeMs - Time in milliseconds to seek to
|
|
192
|
+
* @returns Promise that resolves when the seek is complete and all visible children are ready
|
|
193
|
+
*/
|
|
194
|
+
async seek(timeMs: number): Promise<void> {
|
|
195
|
+
this.currentTimeMs = timeMs;
|
|
196
|
+
await this.seekTask.taskComplete;
|
|
197
|
+
|
|
198
|
+
// Handle localStorage when playbackController delegates seek
|
|
199
|
+
if (this.playbackController) {
|
|
200
|
+
this.saveTimeToLocalStorage(this.currentTime);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await this.frameTask.taskComplete;
|
|
204
|
+
|
|
205
|
+
// Ensure all visible elements have completed their reactive update cycles AND frame rendering
|
|
206
|
+
// waitForFrameTasks() calls frameTask.run() on children, but this may happen before child
|
|
207
|
+
// elements have processed property changes from requestUpdate(). To ensure frame data is
|
|
208
|
+
// accurate, we wait for updateComplete first, then ensure the frameTask has run with the
|
|
209
|
+
// updated properties. Elements like EFVideo provide waitForFrameReady() for this pattern.
|
|
210
|
+
const temporalElements = deepGetElementsWithFrameTasks(this);
|
|
211
|
+
const visibleElements = temporalElements.filter((element) => {
|
|
212
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
213
|
+
return animationState.isVisible;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await Promise.all(
|
|
217
|
+
visibleElements.map(async (element) => {
|
|
218
|
+
if (
|
|
219
|
+
"waitForFrameReady" in element &&
|
|
220
|
+
typeof element.waitForFrameReady === "function"
|
|
221
|
+
) {
|
|
222
|
+
await (element as any).waitForFrameReady();
|
|
223
|
+
} else {
|
|
224
|
+
await element.updateComplete;
|
|
225
|
+
}
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
/**
|
|
209
231
|
* Determines if this is a root timegroup (no parent timegroups)
|
|
210
232
|
*/
|
|
@@ -212,10 +234,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
212
234
|
return !this.parentTimegroup;
|
|
213
235
|
}
|
|
214
236
|
|
|
215
|
-
|
|
216
|
-
* Saves time to localStorage (extracted for reuse)
|
|
217
|
-
*/
|
|
218
|
-
#saveTimeToLocalStorage(time: number) {
|
|
237
|
+
saveTimeToLocalStorage(time: number) {
|
|
219
238
|
try {
|
|
220
239
|
if (this.id && this.isConnected && !Number.isNaN(time)) {
|
|
221
240
|
localStorage.setItem(this.storageKey, time.toString());
|
|
@@ -239,7 +258,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
239
258
|
this.requestUpdate();
|
|
240
259
|
};
|
|
241
260
|
|
|
242
|
-
|
|
261
|
+
loadTimeFromLocalStorage(): number | undefined {
|
|
243
262
|
if (this.id) {
|
|
244
263
|
try {
|
|
245
264
|
const storedValue = localStorage.getItem(this.storageKey);
|
|
@@ -251,21 +270,25 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
251
270
|
log("Failed to load time from localStorage", error);
|
|
252
271
|
}
|
|
253
272
|
}
|
|
273
|
+
return undefined;
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
connectedCallback() {
|
|
257
277
|
super.connectedCallback();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
278
|
+
|
|
279
|
+
if (!this.playbackController) {
|
|
280
|
+
this.waitForMediaDurations().then(() => {
|
|
281
|
+
if (this.id) {
|
|
282
|
+
const maybeLoadedTime = this.loadTimeFromLocalStorage();
|
|
283
|
+
if (maybeLoadedTime !== undefined) {
|
|
284
|
+
this.currentTime = maybeLoadedTime;
|
|
285
|
+
}
|
|
263
286
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}
|
|
287
|
+
if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
|
|
288
|
+
this.seekTask.run();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
269
292
|
|
|
270
293
|
if (this.parentTimegroup) {
|
|
271
294
|
new TimegroupController(this.parentTimegroup, this);
|
|
@@ -278,7 +301,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
278
301
|
|
|
279
302
|
#previousDurationMs = 0;
|
|
280
303
|
|
|
281
|
-
protected updated(
|
|
304
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
305
|
+
super.updated(changedProperties);
|
|
306
|
+
|
|
307
|
+
if (changedProperties.has("mode") || changedProperties.has("overlapMs")) {
|
|
308
|
+
sequenceDurationCache.delete(this);
|
|
309
|
+
}
|
|
310
|
+
|
|
282
311
|
if (this.#previousDurationMs !== this.durationMs) {
|
|
283
312
|
this.#previousDurationMs = this.durationMs;
|
|
284
313
|
this.runThrottledFrameTask();
|
|
@@ -549,13 +578,31 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
549
578
|
/**
|
|
550
579
|
* Returns true if the timegroup should be wrapped with a workbench.
|
|
551
580
|
*
|
|
552
|
-
* A timegroup should be wrapped with a workbench if
|
|
553
|
-
*
|
|
581
|
+
* A timegroup should be wrapped with a workbench if:
|
|
582
|
+
* - It's being rendered (EF_RENDERING), OR
|
|
583
|
+
* - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
|
|
554
584
|
*
|
|
555
|
-
* If the timegroup is already
|
|
585
|
+
* If the timegroup is already wrapped in a context provider like ef-preview,
|
|
556
586
|
* it should NOT be wrapped in a workbench.
|
|
557
587
|
*/
|
|
558
588
|
shouldWrapWithWorkbench() {
|
|
589
|
+
const isRendering = EF_RENDERING?.() === true;
|
|
590
|
+
|
|
591
|
+
// During rendering, always wrap with workbench (needed by EF_FRAMEGEN)
|
|
592
|
+
if (isRendering) {
|
|
593
|
+
return (
|
|
594
|
+
this.closest("ef-timegroup") === this &&
|
|
595
|
+
this.closest("ef-preview") === null &&
|
|
596
|
+
this.closest("ef-workbench") === null &&
|
|
597
|
+
this.closest("test-context") === null
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// During interactive mode, respect the dev workbench flag
|
|
602
|
+
if (!globalThis.EF_DEV_WORKBENCH) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
559
606
|
return (
|
|
560
607
|
EF_INTERACTIVE &&
|
|
561
608
|
this.closest("ef-timegroup") === this &&
|
|
@@ -588,155 +635,22 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
588
635
|
);
|
|
589
636
|
}
|
|
590
637
|
|
|
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
|
-
);
|
|
638
|
+
/**
|
|
639
|
+
* Returns media elements for playback audio rendering
|
|
640
|
+
* For standalone media, returns [this]; for timegroups, returns all descendants
|
|
641
|
+
* Used by PlaybackController for audio-driven playback
|
|
642
|
+
*/
|
|
643
|
+
getMediaElements(): EFMedia[] {
|
|
644
|
+
return deepGetMediaElements(this);
|
|
691
645
|
}
|
|
692
646
|
|
|
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
|
-
);
|
|
647
|
+
/**
|
|
648
|
+
* Render audio buffer for playback
|
|
649
|
+
* Called by PlaybackController during live playback
|
|
650
|
+
* Delegates to shared renderTemporalAudio utility for consistent behavior
|
|
651
|
+
*/
|
|
652
|
+
async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {
|
|
653
|
+
return renderTemporalAudio(this, fromMs, toMs);
|
|
740
654
|
}
|
|
741
655
|
|
|
742
656
|
/**
|
|
@@ -780,7 +694,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
780
694
|
|
|
781
695
|
await Promise.all(loaderTasks);
|
|
782
696
|
|
|
783
|
-
efElements.
|
|
697
|
+
efElements.forEach((el) => {
|
|
784
698
|
if ("productionSrc" in el && el.productionSrc instanceof Function) {
|
|
785
699
|
el.setAttribute("src", el.productionSrc());
|
|
786
700
|
}
|
|
@@ -815,6 +729,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
815
729
|
args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
|
|
816
730
|
onComplete: () => {},
|
|
817
731
|
task: async ([targetTime]) => {
|
|
732
|
+
if (this.playbackController) {
|
|
733
|
+
await this.playbackController.seekTask.taskComplete;
|
|
734
|
+
return this.currentTime;
|
|
735
|
+
}
|
|
736
|
+
|
|
818
737
|
if (!this.isRootTimegroup) {
|
|
819
738
|
return;
|
|
820
739
|
}
|
|
@@ -836,11 +755,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
836
755
|
span.setAttribute("newTime", newTime);
|
|
837
756
|
}
|
|
838
757
|
// Apply the clamped time back to currentTime
|
|
758
|
+
|
|
839
759
|
this.#currentTime = newTime;
|
|
840
760
|
this.requestUpdate("currentTime");
|
|
841
761
|
await this.runThrottledFrameTask();
|
|
842
|
-
this
|
|
843
|
-
// This has to be set false here so any following seeks are not treated as pending
|
|
762
|
+
this.saveTimeToLocalStorage(this.#currentTime);
|
|
844
763
|
this.#seekInProgress = false;
|
|
845
764
|
return newTime;
|
|
846
765
|
},
|