@editframe/elements 0.26.2-beta.0 → 0.26.4-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/elements/EFTimegroup.js +7 -2
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/package.json +2 -2
- package/scripts/build-css.js +3 -3
- package/tsdown.config.ts +1 -1
- package/types.json +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -870
- package/src/elements/EFTimegroup.ts +0 -878
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- package/src/utils/LRUCache.ts +0 -696
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
import { ContextProvider } from "@lit/context";
|
|
2
|
-
import { Task, TaskStatus } from "@lit/task";
|
|
3
|
-
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
4
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
5
|
-
import { currentTimeContext } from "./currentTimeContext.js";
|
|
6
|
-
import { durationContext } from "./durationContext.js";
|
|
7
|
-
import { loopContext, playingContext } from "./playingContext.js";
|
|
8
|
-
|
|
9
|
-
interface PlaybackHost extends HTMLElement, ReactiveControllerHost {
|
|
10
|
-
currentTimeMs: number;
|
|
11
|
-
durationMs: number;
|
|
12
|
-
endTimeMs: number;
|
|
13
|
-
frameTask: { run(): void; taskComplete: Promise<unknown> };
|
|
14
|
-
renderAudio?(fromMs: number, toMs: number): Promise<AudioBuffer>;
|
|
15
|
-
waitForMediaDurations?(): Promise<void>;
|
|
16
|
-
saveTimeToLocalStorage?(time: number): void;
|
|
17
|
-
loadTimeFromLocalStorage?(): number | undefined;
|
|
18
|
-
requestUpdate(property?: string): void;
|
|
19
|
-
updateComplete: Promise<boolean>;
|
|
20
|
-
playing: boolean;
|
|
21
|
-
loop: boolean;
|
|
22
|
-
play(): void;
|
|
23
|
-
pause(): void;
|
|
24
|
-
playbackController?: PlaybackController;
|
|
25
|
-
parentTimegroup?: any;
|
|
26
|
-
rootTimegroup?: any;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type PlaybackControllerUpdateEvent = {
|
|
30
|
-
property: "playing" | "loop" | "currentTimeMs";
|
|
31
|
-
value: boolean | number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Manages playback state and audio-driven timing for root temporal elements
|
|
36
|
-
*
|
|
37
|
-
* Created automatically when a temporal element becomes a root (no parent timegroup)
|
|
38
|
-
* Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants
|
|
39
|
-
* Handles:
|
|
40
|
-
* - Audio-driven playback with Web Audio API
|
|
41
|
-
* - Seek and frame rendering throttling
|
|
42
|
-
* - Time state management with pending seek handling
|
|
43
|
-
* - Playback loop behavior
|
|
44
|
-
*
|
|
45
|
-
* Works with any temporal element (timegroups or standalone media) via PlaybackHost interface
|
|
46
|
-
*/
|
|
47
|
-
export class PlaybackController implements ReactiveController {
|
|
48
|
-
#host: PlaybackHost;
|
|
49
|
-
#playing = false;
|
|
50
|
-
#loop = false;
|
|
51
|
-
#listeners = new Set<(event: PlaybackControllerUpdateEvent) => void>();
|
|
52
|
-
#playingProvider: ContextProvider<typeof playingContext>;
|
|
53
|
-
#loopProvider: ContextProvider<typeof loopContext>;
|
|
54
|
-
#currentTimeMsProvider: ContextProvider<typeof currentTimeContext>;
|
|
55
|
-
#durationMsProvider: ContextProvider<typeof durationContext>;
|
|
56
|
-
|
|
57
|
-
#FPS = 30;
|
|
58
|
-
#MS_PER_FRAME = 1000 / this.#FPS;
|
|
59
|
-
#playbackAudioContext: AudioContext | null = null;
|
|
60
|
-
#playbackAnimationFrameRequest: number | null = null;
|
|
61
|
-
#AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000;
|
|
62
|
-
|
|
63
|
-
#frameTaskInProgress = false;
|
|
64
|
-
#pendingFrameTaskRun = false;
|
|
65
|
-
#processingPendingFrameTask = false;
|
|
66
|
-
|
|
67
|
-
#currentTime: number | undefined = undefined;
|
|
68
|
-
#seekInProgress = false;
|
|
69
|
-
#pendingSeekTime: number | undefined;
|
|
70
|
-
#processingPendingSeek = false;
|
|
71
|
-
#loopingPlayback = false; // Track if we're in a looping playback session
|
|
72
|
-
#playbackWrapTimeSeconds = 0; // The AudioContext time when we wrapped
|
|
73
|
-
|
|
74
|
-
seekTask!: Task<readonly [number | undefined], number | undefined>;
|
|
75
|
-
|
|
76
|
-
constructor(host: PlaybackHost) {
|
|
77
|
-
this.#host = host;
|
|
78
|
-
host.addController(this);
|
|
79
|
-
|
|
80
|
-
this.seekTask = new Task(this.#host, {
|
|
81
|
-
autoRun: false,
|
|
82
|
-
args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
|
|
83
|
-
onComplete: () => {},
|
|
84
|
-
task: async ([targetTime]) => {
|
|
85
|
-
await this.#host.waitForMediaDurations?.();
|
|
86
|
-
const newTime = Math.max(
|
|
87
|
-
0,
|
|
88
|
-
Math.min(targetTime ?? 0, this.#host.durationMs / 1000),
|
|
89
|
-
);
|
|
90
|
-
this.#currentTime = newTime;
|
|
91
|
-
this.#host.requestUpdate("currentTime");
|
|
92
|
-
this.#currentTimeMsProvider.setValue(this.currentTimeMs);
|
|
93
|
-
this.#notifyListeners({
|
|
94
|
-
property: "currentTimeMs",
|
|
95
|
-
value: this.currentTimeMs,
|
|
96
|
-
});
|
|
97
|
-
await this.runThrottledFrameTask();
|
|
98
|
-
this.#host.saveTimeToLocalStorage?.(newTime);
|
|
99
|
-
this.#seekInProgress = false;
|
|
100
|
-
return newTime;
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
this.#playingProvider = new ContextProvider(host, {
|
|
105
|
-
context: playingContext,
|
|
106
|
-
initialValue: this.#playing,
|
|
107
|
-
});
|
|
108
|
-
this.#loopProvider = new ContextProvider(host, {
|
|
109
|
-
context: loopContext,
|
|
110
|
-
initialValue: this.#loop,
|
|
111
|
-
});
|
|
112
|
-
this.#currentTimeMsProvider = new ContextProvider(host, {
|
|
113
|
-
context: currentTimeContext,
|
|
114
|
-
initialValue: host.currentTimeMs,
|
|
115
|
-
});
|
|
116
|
-
this.#durationMsProvider = new ContextProvider(host, {
|
|
117
|
-
context: durationContext,
|
|
118
|
-
initialValue: host.durationMs,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get currentTime(): number {
|
|
123
|
-
const rawTime = this.#currentTime ?? 0;
|
|
124
|
-
// Quantize to frame boundaries based on host's fps
|
|
125
|
-
const fps = (this.#host as any).fps ?? 30;
|
|
126
|
-
if (!fps || fps <= 0) return rawTime;
|
|
127
|
-
const frameDurationS = 1 / fps;
|
|
128
|
-
return Math.round(rawTime / frameDurationS) * frameDurationS;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
set currentTime(time: number) {
|
|
132
|
-
time = Math.max(0, Math.min(this.#host.durationMs / 1000, time));
|
|
133
|
-
if (Number.isNaN(time)) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (time === this.#currentTime && !this.#processingPendingSeek) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (this.#pendingSeekTime === time) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (this.#seekInProgress) {
|
|
144
|
-
this.#pendingSeekTime = time;
|
|
145
|
-
this.#currentTime = time;
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
this.#currentTime = time;
|
|
150
|
-
this.#seekInProgress = true;
|
|
151
|
-
|
|
152
|
-
this.seekTask.run().finally(() => {
|
|
153
|
-
if (
|
|
154
|
-
this.#pendingSeekTime !== undefined &&
|
|
155
|
-
this.#pendingSeekTime !== time
|
|
156
|
-
) {
|
|
157
|
-
const pendingTime = this.#pendingSeekTime;
|
|
158
|
-
this.#pendingSeekTime = undefined;
|
|
159
|
-
this.#processingPendingSeek = true;
|
|
160
|
-
try {
|
|
161
|
-
this.currentTime = pendingTime;
|
|
162
|
-
} finally {
|
|
163
|
-
this.#processingPendingSeek = false;
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
this.#pendingSeekTime = undefined;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
get playing(): boolean {
|
|
172
|
-
return this.#playing;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
setPlaying(value: boolean): void {
|
|
176
|
-
if (this.#playing === value) return;
|
|
177
|
-
this.#playing = value;
|
|
178
|
-
this.#playingProvider.setValue(value);
|
|
179
|
-
this.#host.requestUpdate("playing");
|
|
180
|
-
this.#notifyListeners({ property: "playing", value });
|
|
181
|
-
|
|
182
|
-
if (value) {
|
|
183
|
-
this.startPlayback();
|
|
184
|
-
} else {
|
|
185
|
-
this.stopPlayback();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
get loop(): boolean {
|
|
190
|
-
return this.#loop;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
setLoop(value: boolean): void {
|
|
194
|
-
if (this.#loop === value) return;
|
|
195
|
-
this.#loop = value;
|
|
196
|
-
this.#loopProvider.setValue(value);
|
|
197
|
-
this.#host.requestUpdate("loop");
|
|
198
|
-
this.#notifyListeners({ property: "loop", value });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
get currentTimeMs(): number {
|
|
202
|
-
return this.currentTime * 1000;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
setCurrentTimeMs(value: number): void {
|
|
206
|
-
this.currentTime = value / 1000;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Update time during playback without triggering a seek
|
|
210
|
-
// Used by #syncPlayheadToAudioContext to avoid frame drops
|
|
211
|
-
#updatePlaybackTime(timeMs: number): void {
|
|
212
|
-
const timeSec = timeMs / 1000;
|
|
213
|
-
if (this.#currentTime === timeSec) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
this.#currentTime = timeSec;
|
|
217
|
-
this.#host.requestUpdate("currentTime");
|
|
218
|
-
this.#currentTimeMsProvider.setValue(timeMs);
|
|
219
|
-
this.#notifyListeners({
|
|
220
|
-
property: "currentTimeMs",
|
|
221
|
-
value: timeMs,
|
|
222
|
-
});
|
|
223
|
-
// Trigger frame rendering without the async seek mechanism
|
|
224
|
-
this.runThrottledFrameTask();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
play(): void {
|
|
228
|
-
this.setPlaying(true);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
pause(): void {
|
|
232
|
-
this.setPlaying(false);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
hostConnected(): void {
|
|
236
|
-
if (this.#playing) {
|
|
237
|
-
this.startPlayback();
|
|
238
|
-
} else {
|
|
239
|
-
this.#host.waitForMediaDurations?.().then(() => {
|
|
240
|
-
const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();
|
|
241
|
-
if (maybeLoadedTime !== undefined) {
|
|
242
|
-
this.currentTime = maybeLoadedTime;
|
|
243
|
-
} else if (this.#currentTime === undefined) {
|
|
244
|
-
this.#currentTime = 0;
|
|
245
|
-
}
|
|
246
|
-
if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
|
|
247
|
-
this.seekTask.run();
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
hostDisconnected(): void {
|
|
254
|
-
this.pause();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
hostUpdated(): void {
|
|
258
|
-
this.#durationMsProvider.setValue(this.#host.durationMs);
|
|
259
|
-
this.#currentTimeMsProvider.setValue(this.currentTimeMs);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async runThrottledFrameTask(): Promise<void> {
|
|
263
|
-
if (this.#frameTaskInProgress) {
|
|
264
|
-
this.#pendingFrameTaskRun = true;
|
|
265
|
-
while (this.#frameTaskInProgress) {
|
|
266
|
-
await this.#host.frameTask.taskComplete;
|
|
267
|
-
}
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
this.#frameTaskInProgress = true;
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
await this.#host.frameTask.run();
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.error("Frame task error:", error);
|
|
277
|
-
} finally {
|
|
278
|
-
this.#frameTaskInProgress = false;
|
|
279
|
-
|
|
280
|
-
if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
|
|
281
|
-
this.#pendingFrameTaskRun = false;
|
|
282
|
-
this.#processingPendingFrameTask = true;
|
|
283
|
-
try {
|
|
284
|
-
await this.runThrottledFrameTask();
|
|
285
|
-
} finally {
|
|
286
|
-
this.#processingPendingFrameTask = false;
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
this.#pendingFrameTaskRun = false;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
addListener(listener: (event: PlaybackControllerUpdateEvent) => void): void {
|
|
295
|
-
this.#listeners.add(listener);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
removeListener(
|
|
299
|
-
listener: (event: PlaybackControllerUpdateEvent) => void,
|
|
300
|
-
): void {
|
|
301
|
-
this.#listeners.delete(listener);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
#notifyListeners(event: PlaybackControllerUpdateEvent): void {
|
|
305
|
-
for (const listener of this.#listeners) {
|
|
306
|
-
listener(event);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
remove(): void {
|
|
311
|
-
this.stopPlayback();
|
|
312
|
-
this.#listeners.clear();
|
|
313
|
-
this.#host.removeController(this);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#syncPlayheadToAudioContext(startMs: number) {
|
|
317
|
-
const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;
|
|
318
|
-
const endMs = this.#host.endTimeMs;
|
|
319
|
-
|
|
320
|
-
// Calculate raw time based on audio context
|
|
321
|
-
let rawTimeMs: number;
|
|
322
|
-
if (
|
|
323
|
-
this.#playbackWrapTimeSeconds > 0 &&
|
|
324
|
-
audioContextTime >= this.#playbackWrapTimeSeconds
|
|
325
|
-
) {
|
|
326
|
-
// After wrap: time since wrap, wrapped to duration
|
|
327
|
-
const timeSinceWrap =
|
|
328
|
-
(audioContextTime - this.#playbackWrapTimeSeconds) * 1000;
|
|
329
|
-
rawTimeMs = timeSinceWrap % endMs;
|
|
330
|
-
} else {
|
|
331
|
-
// Before wrap or no wrap: normal calculation
|
|
332
|
-
rawTimeMs = startMs + audioContextTime * 1000;
|
|
333
|
-
|
|
334
|
-
// If looping and we've reached the end, wrap around
|
|
335
|
-
if (this.#loopingPlayback && rawTimeMs >= endMs) {
|
|
336
|
-
rawTimeMs = rawTimeMs % endMs;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const nextTimeMs =
|
|
341
|
-
Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
|
|
342
|
-
|
|
343
|
-
// During playback, update time directly without triggering seek
|
|
344
|
-
// This avoids frame drops at the loop boundary
|
|
345
|
-
this.#updatePlaybackTime(nextTimeMs);
|
|
346
|
-
|
|
347
|
-
// Only check for end if we haven't already handled looping
|
|
348
|
-
if (!this.#loopingPlayback && nextTimeMs >= endMs) {
|
|
349
|
-
this.maybeLoopPlayback();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
|
|
354
|
-
this.#syncPlayheadToAudioContext(startMs);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private async maybeLoopPlayback() {
|
|
359
|
-
if (this.#loop) {
|
|
360
|
-
// Loop enabled: reset to beginning and restart playback
|
|
361
|
-
// We restart the audio system directly without changing #playing state
|
|
362
|
-
// to keep the play button in sync
|
|
363
|
-
this.setCurrentTimeMs(0);
|
|
364
|
-
// Restart in next frame without awaiting to minimize gap
|
|
365
|
-
requestAnimationFrame(() => {
|
|
366
|
-
this.startPlayback();
|
|
367
|
-
});
|
|
368
|
-
} else {
|
|
369
|
-
// No loop: reset to beginning and stop
|
|
370
|
-
// This ensures play button works when clicked again
|
|
371
|
-
this.setCurrentTimeMs(0);
|
|
372
|
-
this.pause();
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private async stopPlayback() {
|
|
377
|
-
if (this.#playbackAudioContext) {
|
|
378
|
-
if (this.#playbackAudioContext.state !== "closed") {
|
|
379
|
-
await this.#playbackAudioContext.close();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (this.#playbackAnimationFrameRequest) {
|
|
383
|
-
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
384
|
-
}
|
|
385
|
-
this.#playbackAudioContext = null;
|
|
386
|
-
this.#playbackAnimationFrameRequest = null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private async startPlayback() {
|
|
390
|
-
await this.stopPlayback();
|
|
391
|
-
const host = this.#host;
|
|
392
|
-
if (!host) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (host.waitForMediaDurations) {
|
|
397
|
-
await host.waitForMediaDurations();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const currentMs = this.currentTimeMs;
|
|
401
|
-
const fromMs = currentMs;
|
|
402
|
-
const toMs = host.endTimeMs;
|
|
403
|
-
|
|
404
|
-
if (fromMs >= toMs) {
|
|
405
|
-
this.pause();
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
let bufferCount = 0;
|
|
410
|
-
this.#playbackAudioContext = new AudioContext({
|
|
411
|
-
latencyHint: "playback",
|
|
412
|
-
});
|
|
413
|
-
this.#loopingPlayback = this.#loop; // Remember if we're in a looping session
|
|
414
|
-
this.#playbackWrapTimeSeconds = 0; // Reset wrap time
|
|
415
|
-
|
|
416
|
-
if (this.#playbackAnimationFrameRequest) {
|
|
417
|
-
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
418
|
-
}
|
|
419
|
-
this.#syncPlayheadToAudioContext(currentMs);
|
|
420
|
-
const playbackContext = this.#playbackAudioContext;
|
|
421
|
-
if (playbackContext.state === "suspended") {
|
|
422
|
-
console.warn(
|
|
423
|
-
"AudioContext is suspended, media playback will not work until user has interacted with page.",
|
|
424
|
-
);
|
|
425
|
-
this.setPlaying(false);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
await playbackContext.suspend();
|
|
429
|
-
|
|
430
|
-
// Track the logical media time (what position in the media we're rendering)
|
|
431
|
-
// vs the AudioContext schedule time (when to play it)
|
|
432
|
-
let logicalTimeMs = currentMs;
|
|
433
|
-
let audioContextTimeMs = 0; // Tracks the schedule position in the AudioContext timeline
|
|
434
|
-
let hasWrapped = false;
|
|
435
|
-
|
|
436
|
-
const fillBuffer = async () => {
|
|
437
|
-
if (bufferCount > 2) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const canFillBuffer = await queueBufferSource();
|
|
441
|
-
if (canFillBuffer) {
|
|
442
|
-
fillBuffer();
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const queueBufferSource = async () => {
|
|
447
|
-
// Check if we've already wrapped and aren't looping anymore
|
|
448
|
-
if (hasWrapped && !this.#loopingPlayback) {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const startMs = logicalTimeMs;
|
|
453
|
-
const endMs = Math.min(
|
|
454
|
-
logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS,
|
|
455
|
-
toMs,
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
// Will this slice reach the end?
|
|
459
|
-
const willReachEnd = endMs >= toMs;
|
|
460
|
-
|
|
461
|
-
if (!host.renderAudio) {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const audioBuffer = await host.renderAudio(startMs, endMs);
|
|
466
|
-
bufferCount++;
|
|
467
|
-
const source = playbackContext.createBufferSource();
|
|
468
|
-
source.buffer = audioBuffer;
|
|
469
|
-
source.connect(playbackContext.destination);
|
|
470
|
-
// Schedule this buffer to play at the current audioContextTime position
|
|
471
|
-
source.start(audioContextTimeMs / 1000);
|
|
472
|
-
|
|
473
|
-
const sliceDurationMs = endMs - startMs;
|
|
474
|
-
|
|
475
|
-
source.onended = () => {
|
|
476
|
-
bufferCount--;
|
|
477
|
-
|
|
478
|
-
if (willReachEnd) {
|
|
479
|
-
if (!this.#loopingPlayback) {
|
|
480
|
-
// Not looping, end playback
|
|
481
|
-
this.maybeLoopPlayback();
|
|
482
|
-
} else {
|
|
483
|
-
// Looping: continue filling buffer after wrap
|
|
484
|
-
fillBuffer();
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
// Continue filling buffer
|
|
488
|
-
fillBuffer();
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
// Advance the AudioContext schedule time
|
|
493
|
-
audioContextTimeMs += sliceDurationMs;
|
|
494
|
-
|
|
495
|
-
// If this buffer reaches the end and we're looping, immediately queue the wraparound
|
|
496
|
-
if (willReachEnd && this.#loopingPlayback) {
|
|
497
|
-
// Mark that we've wrapped
|
|
498
|
-
hasWrapped = true;
|
|
499
|
-
// Store when we wrapped (relative to when playback started, which is time 0 in AudioContext)
|
|
500
|
-
// This is the duration from start to end
|
|
501
|
-
this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1000;
|
|
502
|
-
// Reset logical time to beginning
|
|
503
|
-
logicalTimeMs = 0;
|
|
504
|
-
// Continue buffering will happen in fillBuffer() call below
|
|
505
|
-
} else {
|
|
506
|
-
// Normal advance
|
|
507
|
-
logicalTimeMs = endMs;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return true;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
try {
|
|
514
|
-
await fillBuffer();
|
|
515
|
-
await playbackContext.resume();
|
|
516
|
-
} catch (error) {
|
|
517
|
-
// Ignore errors if AudioContext is closed or during test cleanup
|
|
518
|
-
if (
|
|
519
|
-
error instanceof Error &&
|
|
520
|
-
(error.name === "InvalidStateError" || error.message.includes("closed"))
|
|
521
|
-
) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
throw error;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
package/src/gui/TWMixin.css
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */
|
|
2
|
-
@tailwind base;
|
|
3
|
-
/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */
|
|
4
|
-
@tailwind components;
|
|
5
|
-
/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */
|
|
6
|
-
@tailwind utilities;
|
package/src/gui/TWMixin.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { CSSResult, LitElement } from "lit";
|
|
2
|
-
// @ts-expect-error cannot figure out how to declare this module as a string
|
|
3
|
-
import twStyle from "./TWMixin.css?inline";
|
|
4
|
-
|
|
5
|
-
let twSheet: CSSStyleSheet | null = null;
|
|
6
|
-
if (typeof window !== "undefined" && typeof CSSStyleSheet !== "undefined") {
|
|
7
|
-
try {
|
|
8
|
-
twSheet = new CSSStyleSheet();
|
|
9
|
-
if (typeof twSheet.replaceSync === "function") {
|
|
10
|
-
twSheet.replaceSync(twStyle);
|
|
11
|
-
}
|
|
12
|
-
} catch (_error) {
|
|
13
|
-
// CSSStyleSheet or replaceSync not supported in this environment
|
|
14
|
-
twSheet = null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function TWMixin<T extends new (...args: any[]) => LitElement>(Base: T) {
|
|
18
|
-
class TWElement extends Base {
|
|
19
|
-
createRenderRoot() {
|
|
20
|
-
const renderRoot = super.createRenderRoot();
|
|
21
|
-
if (!(renderRoot instanceof ShadowRoot)) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
"TWMixin can only be applied to elements with shadow roots",
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
if (!twSheet) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
"twSheet not found. Probable cause: CSSStyleSheet not supported in this environment",
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const constructorStylesheets: CSSStyleSheet[] = [];
|
|
33
|
-
const constructorStyles = (("styles" in this.constructor &&
|
|
34
|
-
this.constructor.styles) ||
|
|
35
|
-
[]) as CSSResult | CSSResult[];
|
|
36
|
-
|
|
37
|
-
if (Array.isArray(constructorStyles)) {
|
|
38
|
-
for (const item of constructorStyles) {
|
|
39
|
-
if (item.styleSheet) {
|
|
40
|
-
constructorStylesheets.push(item.styleSheet);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
} else if (constructorStyles.styleSheet) {
|
|
44
|
-
constructorStylesheets.push(constructorStyles.styleSheet);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (renderRoot?.adoptedStyleSheets) {
|
|
48
|
-
renderRoot.adoptedStyleSheets = [
|
|
49
|
-
twSheet,
|
|
50
|
-
...renderRoot.adoptedStyleSheets,
|
|
51
|
-
...constructorStylesheets,
|
|
52
|
-
];
|
|
53
|
-
} else {
|
|
54
|
-
renderRoot.adoptedStyleSheets = [twSheet, ...constructorStylesheets];
|
|
55
|
-
}
|
|
56
|
-
return renderRoot;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return TWElement as T;
|
|
61
|
-
}
|