@editframe/elements 0.26.3-beta.0 → 0.30.0-beta.13

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.
Files changed (191) hide show
  1. package/dist/elements/EFSourceMixin.js +1 -1
  2. package/dist/elements/EFSourceMixin.js.map +1 -1
  3. package/dist/elements/EFSurface.d.ts +4 -4
  4. package/dist/elements/EFText.d.ts +52 -0
  5. package/dist/elements/EFText.js +319 -0
  6. package/dist/elements/EFText.js.map +1 -0
  7. package/dist/elements/EFTextSegment.d.ts +30 -0
  8. package/dist/elements/EFTextSegment.js +94 -0
  9. package/dist/elements/EFTextSegment.js.map +1 -0
  10. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  11. package/dist/elements/EFWaveform.d.ts +4 -4
  12. package/dist/elements/FetchMixin.js +22 -7
  13. package/dist/elements/FetchMixin.js.map +1 -1
  14. package/dist/elements/easingUtils.js +62 -0
  15. package/dist/elements/easingUtils.js.map +1 -0
  16. package/dist/elements/updateAnimations.js +57 -10
  17. package/dist/elements/updateAnimations.js.map +1 -1
  18. package/dist/gui/ContextMixin.js +11 -2
  19. package/dist/gui/ContextMixin.js.map +1 -1
  20. package/dist/gui/EFConfiguration.d.ts +4 -4
  21. package/dist/gui/EFControls.d.ts +2 -2
  22. package/dist/gui/EFDial.d.ts +4 -4
  23. package/dist/gui/EFDial.js +4 -2
  24. package/dist/gui/EFDial.js.map +1 -1
  25. package/dist/gui/EFFilmstrip.d.ts +32 -6
  26. package/dist/gui/EFFilmstrip.js +314 -50
  27. package/dist/gui/EFFilmstrip.js.map +1 -1
  28. package/dist/gui/EFFitScale.js +39 -15
  29. package/dist/gui/EFFitScale.js.map +1 -1
  30. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  31. package/dist/gui/EFPause.d.ts +4 -4
  32. package/dist/gui/EFPlay.d.ts +4 -4
  33. package/dist/gui/EFPreview.d.ts +4 -4
  34. package/dist/gui/EFPreview.js +2 -2
  35. package/dist/gui/EFPreview.js.map +1 -1
  36. package/dist/gui/EFResizableBox.d.ts +4 -4
  37. package/dist/gui/EFResizableBox.js +6 -3
  38. package/dist/gui/EFResizableBox.js.map +1 -1
  39. package/dist/gui/EFScrubber.d.ts +8 -5
  40. package/dist/gui/EFScrubber.js +64 -12
  41. package/dist/gui/EFScrubber.js.map +1 -1
  42. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  43. package/dist/gui/EFToggleLoop.d.ts +4 -4
  44. package/dist/gui/EFTogglePlay.d.ts +4 -4
  45. package/dist/gui/EFWorkbench.d.ts +4 -4
  46. package/dist/gui/EFWorkbench.js +16 -3
  47. package/dist/gui/EFWorkbench.js.map +1 -1
  48. package/dist/gui/TWMixin.js +1 -1
  49. package/dist/gui/TWMixin.js.map +1 -1
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/style.css +7 -120
  54. package/package.json +3 -3
  55. package/scripts/build-css.js +5 -9
  56. package/test/constants.ts +8 -0
  57. package/test/recordReplayProxyPlugin.js +76 -10
  58. package/test/setup.ts +32 -0
  59. package/test/useMSW.ts +3 -0
  60. package/test/useTranscodeMSW.ts +191 -0
  61. package/tsdown.config.ts +7 -5
  62. package/types.json +1 -1
  63. package/src/elements/ContextProxiesController.ts +0 -124
  64. package/src/elements/CrossUpdateController.ts +0 -22
  65. package/src/elements/EFAudio.browsertest.ts +0 -706
  66. package/src/elements/EFAudio.ts +0 -56
  67. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  68. package/src/elements/EFCaptions.ts +0 -823
  69. package/src/elements/EFImage.browsertest.ts +0 -120
  70. package/src/elements/EFImage.ts +0 -113
  71. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  72. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  73. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  74. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  75. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  76. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  77. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  78. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  79. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  80. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  81. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  82. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  83. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  84. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  85. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  86. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  87. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  88. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  89. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  90. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  91. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  92. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  93. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  94. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  95. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  96. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  97. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  98. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  99. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  100. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  101. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  102. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  103. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  104. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  105. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  106. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  107. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  108. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  109. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  110. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  111. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  112. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  113. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  114. package/src/elements/EFMedia.browsertest.ts +0 -872
  115. package/src/elements/EFMedia.ts +0 -341
  116. package/src/elements/EFSourceMixin.ts +0 -60
  117. package/src/elements/EFSurface.browsertest.ts +0 -151
  118. package/src/elements/EFSurface.ts +0 -142
  119. package/src/elements/EFTemporal.browsertest.ts +0 -215
  120. package/src/elements/EFTemporal.ts +0 -800
  121. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  122. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  123. package/src/elements/EFThumbnailStrip.ts +0 -906
  124. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  125. package/src/elements/EFTimegroup.ts +0 -882
  126. package/src/elements/EFVideo.browsertest.ts +0 -1482
  127. package/src/elements/EFVideo.ts +0 -564
  128. package/src/elements/EFWaveform.ts +0 -547
  129. package/src/elements/FetchContext.browsertest.ts +0 -401
  130. package/src/elements/FetchMixin.ts +0 -38
  131. package/src/elements/SampleBuffer.ts +0 -94
  132. package/src/elements/TargetController.browsertest.ts +0 -230
  133. package/src/elements/TargetController.ts +0 -224
  134. package/src/elements/TimegroupController.ts +0 -26
  135. package/src/elements/durationConverter.ts +0 -35
  136. package/src/elements/parseTimeToMs.ts +0 -9
  137. package/src/elements/printTaskStatus.ts +0 -16
  138. package/src/elements/renderTemporalAudio.ts +0 -108
  139. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  140. package/src/elements/updateAnimations.ts +0 -217
  141. package/src/elements/util.ts +0 -24
  142. package/src/gui/ContextMixin.browsertest.ts +0 -860
  143. package/src/gui/ContextMixin.ts +0 -562
  144. package/src/gui/Controllable.browsertest.ts +0 -258
  145. package/src/gui/Controllable.ts +0 -41
  146. package/src/gui/EFConfiguration.ts +0 -40
  147. package/src/gui/EFControls.browsertest.ts +0 -389
  148. package/src/gui/EFControls.ts +0 -195
  149. package/src/gui/EFDial.browsertest.ts +0 -84
  150. package/src/gui/EFDial.ts +0 -172
  151. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  152. package/src/gui/EFFilmstrip.ts +0 -1349
  153. package/src/gui/EFFitScale.ts +0 -152
  154. package/src/gui/EFFocusOverlay.ts +0 -79
  155. package/src/gui/EFPause.browsertest.ts +0 -202
  156. package/src/gui/EFPause.ts +0 -73
  157. package/src/gui/EFPlay.browsertest.ts +0 -202
  158. package/src/gui/EFPlay.ts +0 -73
  159. package/src/gui/EFPreview.ts +0 -74
  160. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  161. package/src/gui/EFResizableBox.ts +0 -898
  162. package/src/gui/EFScrubber.ts +0 -151
  163. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  164. package/src/gui/EFTimeDisplay.ts +0 -55
  165. package/src/gui/EFToggleLoop.ts +0 -35
  166. package/src/gui/EFTogglePlay.ts +0 -70
  167. package/src/gui/EFWorkbench.ts +0 -115
  168. package/src/gui/PlaybackController.ts +0 -527
  169. package/src/gui/TWMixin.css +0 -6
  170. package/src/gui/TWMixin.ts +0 -61
  171. package/src/gui/TargetOrContextMixin.ts +0 -185
  172. package/src/gui/currentTimeContext.ts +0 -5
  173. package/src/gui/durationContext.ts +0 -3
  174. package/src/gui/efContext.ts +0 -6
  175. package/src/gui/fetchContext.ts +0 -5
  176. package/src/gui/focusContext.ts +0 -7
  177. package/src/gui/focusedElementContext.ts +0 -5
  178. package/src/gui/playingContext.ts +0 -5
  179. package/src/otel/BridgeSpanExporter.ts +0 -150
  180. package/src/otel/setupBrowserTracing.ts +0 -73
  181. package/src/otel/tracingHelpers.ts +0 -251
  182. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  183. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  184. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  185. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  186. package/src/transcoding/types/index.ts +0 -312
  187. package/src/transcoding/utils/MediaUtils.ts +0 -63
  188. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  189. package/src/transcoding/utils/constants.ts +0 -36
  190. package/src/utils/LRUCache.test.ts +0 -274
  191. 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
- }
@@ -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;
@@ -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
- }