@editframe/elements 0.6.0-beta.9 → 0.7.0-beta.4

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 (102) hide show
  1. package/dist/lib/av/EncodedAsset.cjs +577 -0
  2. package/dist/lib/av/EncodedAsset.js +560 -0
  3. package/dist/lib/av/MP4File.cjs +187 -0
  4. package/dist/lib/av/MP4File.js +170 -0
  5. package/dist/lib/av/msToTimeCode.cjs +15 -0
  6. package/dist/lib/av/msToTimeCode.js +15 -0
  7. package/dist/lib/util/awaitMicrotask.cjs +4 -0
  8. package/dist/lib/util/awaitMicrotask.js +4 -0
  9. package/dist/lib/util/memoize.cjs +14 -0
  10. package/dist/lib/util/memoize.js +14 -0
  11. package/dist/packages/elements/src/EF_FRAMEGEN.cjs +197 -0
  12. package/dist/packages/elements/src/EF_FRAMEGEN.d.ts +44 -0
  13. package/dist/packages/elements/src/EF_FRAMEGEN.js +197 -0
  14. package/dist/packages/elements/src/EF_INTERACTIVE.cjs +4 -0
  15. package/dist/packages/elements/src/EF_INTERACTIVE.d.ts +1 -0
  16. package/dist/packages/elements/src/EF_INTERACTIVE.js +4 -0
  17. package/dist/packages/elements/src/elements/CrossUpdateController.cjs +16 -0
  18. package/dist/packages/elements/src/elements/CrossUpdateController.d.ts +9 -0
  19. package/dist/packages/elements/src/elements/CrossUpdateController.js +16 -0
  20. package/dist/packages/elements/src/elements/EFAudio.cjs +53 -0
  21. package/dist/packages/elements/src/elements/EFAudio.d.ts +10 -0
  22. package/dist/packages/elements/src/elements/EFAudio.js +54 -0
  23. package/dist/packages/elements/src/elements/EFCaptions.cjs +164 -0
  24. package/dist/packages/elements/src/elements/EFCaptions.d.ts +38 -0
  25. package/dist/packages/elements/src/elements/EFCaptions.js +166 -0
  26. package/dist/packages/elements/src/elements/EFImage.cjs +79 -0
  27. package/dist/packages/elements/src/elements/EFImage.d.ts +14 -0
  28. package/dist/packages/elements/src/elements/EFImage.js +80 -0
  29. package/dist/packages/elements/src/elements/EFMedia.cjs +336 -0
  30. package/dist/packages/elements/src/elements/EFMedia.d.ts +61 -0
  31. package/dist/packages/elements/src/elements/EFMedia.js +336 -0
  32. package/dist/packages/elements/src/elements/EFSourceMixin.cjs +55 -0
  33. package/dist/packages/elements/src/elements/EFSourceMixin.d.ts +12 -0
  34. package/dist/packages/elements/src/elements/EFSourceMixin.js +55 -0
  35. package/dist/packages/elements/src/elements/EFTemporal.cjs +199 -0
  36. package/dist/packages/elements/src/elements/EFTemporal.d.ts +38 -0
  37. package/dist/packages/elements/src/elements/EFTemporal.js +199 -0
  38. package/dist/packages/elements/src/elements/EFTimegroup.browsertest.d.ts +12 -0
  39. package/dist/packages/elements/src/elements/EFTimegroup.cjs +352 -0
  40. package/dist/packages/elements/src/elements/EFTimegroup.d.ts +39 -0
  41. package/dist/packages/elements/src/elements/EFTimegroup.js +353 -0
  42. package/dist/packages/elements/src/elements/EFTimeline.cjs +15 -0
  43. package/dist/packages/elements/src/elements/EFTimeline.d.ts +3 -0
  44. package/dist/packages/elements/src/elements/EFTimeline.js +15 -0
  45. package/dist/packages/elements/src/elements/EFVideo.cjs +109 -0
  46. package/dist/packages/elements/src/elements/EFVideo.d.ts +14 -0
  47. package/dist/packages/elements/src/elements/EFVideo.js +110 -0
  48. package/dist/packages/elements/src/elements/EFWaveform.cjs +242 -0
  49. package/dist/packages/elements/src/elements/EFWaveform.d.ts +30 -0
  50. package/dist/packages/elements/src/elements/EFWaveform.js +226 -0
  51. package/dist/packages/elements/src/elements/FetchMixin.cjs +28 -0
  52. package/dist/packages/elements/src/elements/FetchMixin.d.ts +8 -0
  53. package/dist/packages/elements/src/elements/FetchMixin.js +28 -0
  54. package/dist/packages/elements/src/elements/TimegroupController.cjs +20 -0
  55. package/dist/packages/elements/src/elements/TimegroupController.d.ts +14 -0
  56. package/dist/packages/elements/src/elements/TimegroupController.js +20 -0
  57. package/dist/packages/elements/src/elements/durationConverter.cjs +8 -0
  58. package/dist/packages/elements/src/elements/durationConverter.d.ts +4 -0
  59. package/dist/packages/elements/src/elements/durationConverter.js +8 -0
  60. package/dist/packages/elements/src/elements/parseTimeToMs.cjs +12 -0
  61. package/dist/packages/elements/src/elements/parseTimeToMs.d.ts +1 -0
  62. package/dist/packages/elements/src/elements/parseTimeToMs.js +12 -0
  63. package/dist/packages/elements/src/elements/util.cjs +11 -0
  64. package/dist/packages/elements/src/elements/util.d.ts +4 -0
  65. package/dist/packages/elements/src/elements/util.js +11 -0
  66. package/dist/packages/elements/src/gui/EFFilmstrip.cjs +825 -0
  67. package/dist/packages/elements/src/gui/EFFilmstrip.d.ts +147 -0
  68. package/dist/packages/elements/src/gui/EFFilmstrip.js +833 -0
  69. package/dist/packages/elements/src/gui/EFWorkbench.cjs +214 -0
  70. package/dist/packages/elements/src/gui/EFWorkbench.d.ts +45 -0
  71. package/dist/packages/elements/src/gui/EFWorkbench.js +215 -0
  72. package/dist/packages/elements/src/gui/TWMixin.cjs +28 -0
  73. package/dist/packages/elements/src/gui/TWMixin.css.cjs +3 -0
  74. package/dist/packages/elements/src/gui/TWMixin.css.js +4 -0
  75. package/dist/packages/elements/src/gui/TWMixin.d.ts +3 -0
  76. package/dist/packages/elements/src/gui/TWMixin.js +28 -0
  77. package/dist/packages/elements/src/index.cjs +52 -0
  78. package/dist/packages/elements/src/index.d.ts +11 -0
  79. package/dist/packages/elements/src/index.js +25 -0
  80. package/dist/style.css +791 -0
  81. package/package.json +14 -8
  82. package/src/elements/CrossUpdateController.ts +22 -0
  83. package/src/elements/EFAudio.ts +40 -0
  84. package/src/elements/EFCaptions.ts +188 -0
  85. package/src/elements/EFImage.ts +68 -0
  86. package/src/elements/EFMedia.ts +389 -0
  87. package/src/elements/EFSourceMixin.ts +57 -0
  88. package/src/elements/EFTemporal.ts +234 -0
  89. package/src/elements/EFTimegroup.browsertest.ts +333 -0
  90. package/src/elements/EFTimegroup.ts +393 -0
  91. package/src/elements/EFTimeline.ts +13 -0
  92. package/src/elements/EFVideo.ts +103 -0
  93. package/src/elements/EFWaveform.ts +417 -0
  94. package/src/elements/FetchMixin.ts +19 -0
  95. package/src/elements/TimegroupController.ts +25 -0
  96. package/src/elements/durationConverter.ts +6 -0
  97. package/src/elements/parseTimeToMs.ts +9 -0
  98. package/src/elements/util.ts +24 -0
  99. package/src/gui/EFFilmstrip.ts +884 -0
  100. package/src/gui/EFWorkbench.ts +233 -0
  101. package/src/gui/TWMixin.css +3 -0
  102. package/src/gui/TWMixin.ts +30 -0
@@ -0,0 +1,393 @@
1
+ import { LitElement, html, css, type PropertyValueMap } from "lit";
2
+ import { provide } from "@lit/context";
3
+ import { Task } from "@lit/task";
4
+ import { customElement, property } from "lit/decorators.js";
5
+ import debug from "debug";
6
+
7
+ import {
8
+ EFTemporal,
9
+ isEFTemporal,
10
+ shallowGetTemporalElements,
11
+ timegroupContext,
12
+ } from "./EFTemporal";
13
+ import { TimegroupController } from "./TimegroupController";
14
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
15
+ import { deepGetMediaElements } from "./EFMedia";
16
+
17
+ const log = debug("ef:elements:EFTimegroup");
18
+
19
+ export const shallowGetTimegroups = (
20
+ element: Element,
21
+ groups: EFTimegroup[] = [],
22
+ ) => {
23
+ for (const child of Array.from(element.children)) {
24
+ if (child instanceof EFTimegroup) {
25
+ groups.push(child);
26
+ } else {
27
+ shallowGetTimegroups(child, groups);
28
+ }
29
+ }
30
+ return groups;
31
+ };
32
+
33
+ @customElement("ef-timegroup")
34
+ export class EFTimegroup extends EFTemporal(LitElement) {
35
+ static styles = css`
36
+ :host {
37
+ display: block;
38
+ width: 100%;
39
+ height: 100%;
40
+ position: relative;
41
+ top: 0;
42
+ }
43
+ `;
44
+
45
+ @provide({ context: timegroupContext })
46
+ _timeGroupContext = this;
47
+
48
+ #currentTime = 0;
49
+
50
+ @property({
51
+ type: String,
52
+ attribute: "mode",
53
+ })
54
+ mode: "fixed" | "sequence" | "contain" = "sequence";
55
+
56
+ @property({ type: Number })
57
+ set currentTime(time: number) {
58
+ this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
59
+ try {
60
+ if (this.id) {
61
+ if (this.isConnected) {
62
+ localStorage.setItem(this.storageKey, time.toString());
63
+ }
64
+ }
65
+ } catch (error) {
66
+ log("Failed to save time to localStorage", error);
67
+ }
68
+ }
69
+ get currentTime() {
70
+ return this.#currentTime;
71
+ }
72
+ get currentTimeMs() {
73
+ return this.currentTime * 1000;
74
+ }
75
+ set currentTimeMs(ms: number) {
76
+ this.currentTime = ms / 1000;
77
+ }
78
+
79
+ @property({
80
+ attribute: "crossover",
81
+ converter: {
82
+ fromAttribute: (value: string): number => {
83
+ if (value.endsWith("ms")) {
84
+ return Number.parseFloat(value);
85
+ }
86
+ if (value.endsWith("s")) {
87
+ return Number.parseFloat(value) * 1000;
88
+ }
89
+ throw new Error(
90
+ "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
91
+ );
92
+ },
93
+ toAttribute: (value: number) => `${value}ms`,
94
+ },
95
+ })
96
+ crossoverMs = 0;
97
+
98
+ render() {
99
+ return html`<slot></slot> `;
100
+ }
101
+
102
+ maybeLoadTimeFromLocalStorage() {
103
+ if (this.id) {
104
+ try {
105
+ return Number.parseFloat(localStorage.getItem(this.storageKey) || "0");
106
+ } catch (error) {
107
+ log("Failed to load time from localStorage", error);
108
+ }
109
+ }
110
+ return 0;
111
+ }
112
+
113
+ connectedCallback() {
114
+ super.connectedCallback();
115
+ if (this.id) {
116
+ this.#currentTime = this.maybeLoadTimeFromLocalStorage();
117
+ }
118
+
119
+ if (this.parentTimegroup) {
120
+ new TimegroupController(this.parentTimegroup, this);
121
+ }
122
+
123
+ if (this.shouldWrapWithWorkbench()) {
124
+ this.wrapWithWorkbench();
125
+ }
126
+ }
127
+
128
+ get storageKey() {
129
+ if (!this.id) {
130
+ throw new Error("Timegroup must have an id to use localStorage.");
131
+ }
132
+ return `ef-timegroup-${this.id}`;
133
+ }
134
+
135
+ get crossoverStartMs() {
136
+ const parentTimeGroup = this.parentTimegroup;
137
+ if (!parentTimeGroup || !this.previousElementSibling) {
138
+ return 0;
139
+ }
140
+
141
+ return parentTimeGroup.crossoverMs;
142
+ }
143
+ get crossoverEndMs() {
144
+ const parentTimeGroup = this.parentTimegroup;
145
+ if (!parentTimeGroup || !this.nextElementSibling) {
146
+ return 0;
147
+ }
148
+
149
+ return parentTimeGroup.crossoverMs;
150
+ }
151
+
152
+ get durationMs() {
153
+ switch (this.mode) {
154
+ case "fixed":
155
+ return super.durationMs;
156
+ case "sequence": {
157
+ let duration = 0;
158
+ for (const node of this.childTemporals) {
159
+ duration += node.durationMs;
160
+ }
161
+ return duration;
162
+ }
163
+ case "contain": {
164
+ let maxDuration = 0;
165
+ for (const node of this.childTemporals) {
166
+ if (node.hasOwnDuration) {
167
+ maxDuration = Math.max(maxDuration, node.durationMs);
168
+ }
169
+ }
170
+ return maxDuration;
171
+ }
172
+ default:
173
+ throw new Error(`Invalid time mode: ${this.mode}`);
174
+ }
175
+ }
176
+
177
+ async waitForMediaDurations() {
178
+ return await Promise.all(
179
+ deepGetMediaElements(this).map(
180
+ (media) => media.trackFragmentIndexLoader.taskComplete,
181
+ ),
182
+ );
183
+ }
184
+
185
+ get childTemporals() {
186
+ return shallowGetTemporalElements(this);
187
+ }
188
+
189
+ protected updated(
190
+ changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
191
+ ): void {
192
+ super.updated(changedProperties);
193
+ if (
194
+ changedProperties.has("currentTime") ||
195
+ changedProperties.has("ownCurrentTimeMs")
196
+ ) {
197
+ const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
198
+ if (
199
+ this.startTimeMs > timelineTimeMs ||
200
+ this.endTimeMs < timelineTimeMs
201
+ ) {
202
+ this.style.display = "none";
203
+ return;
204
+ }
205
+ this.style.display = "";
206
+
207
+ const animations = this.getAnimations({ subtree: true });
208
+ this.style.setProperty(
209
+ "--ef-duration",
210
+ `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
211
+ );
212
+
213
+ for (const animation of animations) {
214
+ if (animation.playState === "running") {
215
+ animation.pause();
216
+ }
217
+ const effect = animation.effect;
218
+ if (!(effect && effect instanceof KeyframeEffect)) {
219
+ return;
220
+ }
221
+ const target = effect.target;
222
+ // TODO: better generalize work avoidance for temporal elements
223
+ if (!target) {
224
+ return;
225
+ }
226
+ if (target.closest("ef-timegroup") !== this) {
227
+ return;
228
+ }
229
+
230
+ // Important to avoid going to the end of the animation
231
+ // or it will reset awkwardly.
232
+ if (isEFTemporal(target)) {
233
+ const timing = effect.getTiming();
234
+ const duration = Number(timing.duration) ?? 0;
235
+ const delay = Number(timing.delay);
236
+ const newTime = Math.floor(
237
+ Math.min(target.ownCurrentTimeMs, duration - 1 + delay),
238
+ );
239
+ if (Number.isNaN(newTime)) {
240
+ return;
241
+ }
242
+ animation.currentTime = newTime;
243
+ } else if (target) {
244
+ const nearestTimegroup = target.closest("ef-timegroup");
245
+ if (!nearestTimegroup) {
246
+ return;
247
+ }
248
+ const timing = effect.getTiming();
249
+ const duration = Number(timing.duration) ?? 0;
250
+ const delay = Number(timing.delay);
251
+ const newTime = Math.floor(
252
+ Math.min(nearestTimegroup.ownCurrentTimeMs, duration - 1 + delay),
253
+ );
254
+
255
+ if (Number.isNaN(newTime)) {
256
+ return;
257
+ }
258
+ animation.currentTime = newTime;
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ shouldWrapWithWorkbench() {
265
+ return (
266
+ EF_INTERACTIVE &&
267
+ this.closest("ef-timegroup") === this &&
268
+ this.closest("ef-workbench") === null
269
+ );
270
+ }
271
+
272
+ wrapWithWorkbench() {
273
+ const workbench = document.createElement("ef-workbench");
274
+ document.body.append(workbench);
275
+ if (!this.hasAttribute("id")) {
276
+ this.setAttribute("id", "root-this");
277
+ }
278
+ this.setAttribute("slot", "canvas");
279
+ workbench.append(this as unknown as Element);
280
+
281
+ const filmstrip = document.createElement("ef-filmstrip");
282
+ filmstrip.setAttribute("slot", "timeline");
283
+ filmstrip.setAttribute("target", this.id);
284
+ workbench.append(filmstrip);
285
+ }
286
+
287
+ get hasOwnDuration() {
288
+ return true;
289
+ }
290
+
291
+ get efElements() {
292
+ return Array.from(
293
+ this.querySelectorAll(
294
+ "ef-audio, ef-video, ef-image, ef-captions, ef-waveform",
295
+ ),
296
+ );
297
+ }
298
+
299
+ async #addAudioToContext(
300
+ audioContext: AudioContext | OfflineAudioContext,
301
+ fromMs: number,
302
+ toMs: number,
303
+ ) {
304
+ await this.waitForMediaDurations();
305
+
306
+ const durationMs = toMs - fromMs;
307
+
308
+ await Promise.all(
309
+ deepGetMediaElements(this).map(async (mediaElement) => {
310
+ await mediaElement.trackFragmentIndexLoader.taskComplete;
311
+
312
+ const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
313
+ const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
314
+ const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
315
+ if (!mediaOverlaps || mediaElement.defaultAudioTrackId === undefined) {
316
+ return;
317
+ }
318
+
319
+ const audio = await mediaElement.fetchAudioSpanningTime(fromMs, toMs);
320
+ if (!audio) {
321
+ throw new Error("Failed to fetch audio");
322
+ }
323
+
324
+ const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
325
+ const ctxEndMs = Math.min(durationMs, mediaElement.endTimeMs - fromMs);
326
+ const ctxDurationMs = ctxEndMs - ctxStartMs;
327
+
328
+ const offset =
329
+ Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
330
+
331
+ const bufferSource = audioContext.createBufferSource();
332
+ bufferSource.buffer = await audioContext.decodeAudioData(
333
+ await audio.blob.arrayBuffer(),
334
+ );
335
+ bufferSource.connect(audioContext.destination);
336
+
337
+ bufferSource.start(
338
+ ctxStartMs / 1000,
339
+ offset / 1000,
340
+ ctxDurationMs / 1000,
341
+ );
342
+ }),
343
+ );
344
+ }
345
+
346
+ async renderAudio(fromMs: number, toMs: number) {
347
+ const durationMs = toMs - fromMs;
348
+ const audioContext = new OfflineAudioContext(
349
+ 2,
350
+ Math.round((48000 * durationMs) / 1000),
351
+ 48000,
352
+ );
353
+ await this.#addAudioToContext(audioContext, fromMs, toMs);
354
+ return await audioContext.startRendering();
355
+ }
356
+
357
+ async loadMd5Sums() {
358
+ const efElements = this.efElements;
359
+ const loaderTasks: Promise<any>[] = [];
360
+ for (const el of efElements) {
361
+ const md5SumLoader = (el as any).md5SumLoader;
362
+ if (md5SumLoader instanceof Task) {
363
+ md5SumLoader.run();
364
+ loaderTasks.push(md5SumLoader.taskComplete);
365
+ }
366
+ }
367
+
368
+ await Promise.all(loaderTasks);
369
+
370
+ efElements.map((el) => {
371
+ if ("productionSrc" in el && el.productionSrc instanceof Function) {
372
+ el.setAttribute("src", el.productionSrc());
373
+ }
374
+ });
375
+ }
376
+
377
+ frameTask = new Task(this, {
378
+ autoRun: EF_INTERACTIVE,
379
+ args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
380
+ task: async ([], { signal: _signal }) => {
381
+ let fullyUpdated = await this.updateComplete;
382
+ while (!fullyUpdated) {
383
+ fullyUpdated = await this.updateComplete;
384
+ }
385
+ },
386
+ });
387
+ }
388
+
389
+ declare global {
390
+ interface HTMLElementTagNameMap {
391
+ "ef-timegroup": EFTimegroup & Element;
392
+ }
393
+ }
@@ -0,0 +1,13 @@
1
+ export class EFTimeline extends HTMLElement {
2
+ get durationMs() {
3
+ let duration = 0;
4
+ for (const node of this.childNodes) {
5
+ if ("durationMs" in node && typeof node.durationMs === "number") {
6
+ duration += node.durationMs;
7
+ }
8
+ }
9
+ return duration;
10
+ }
11
+ }
12
+
13
+ window.customElements.define("ef-timeline", EFTimeline);
@@ -0,0 +1,103 @@
1
+ import { html, css } from "lit";
2
+ import { Task } from "@lit/task";
3
+ import { createRef, ref } from "lit/directives/ref.js";
4
+ import { customElement } from "lit/decorators.js";
5
+
6
+ import { EFMedia } from "./EFMedia";
7
+ import { TWMixin } from "../gui/TWMixin";
8
+
9
+ @customElement("ef-video")
10
+ export class EFVideo extends TWMixin(EFMedia) {
11
+ static styles = [
12
+ css`
13
+ :host {
14
+ display: block;
15
+ }
16
+ `,
17
+ ];
18
+ canvasRef = createRef<HTMLCanvasElement>();
19
+
20
+ render() {
21
+ return html` <canvas
22
+ class="h-full w-full object-fill"
23
+ ${ref(this.canvasRef)}
24
+ ></canvas>`;
25
+ }
26
+
27
+ get canvasElement() {
28
+ return this.canvasRef.value;
29
+ }
30
+
31
+ // The underlying video decoder MUST NOT be used concurrently.
32
+ // If frames are fed in out of order, the decoder may crash.
33
+ #decoderLock = false;
34
+
35
+ frameTask = new Task(this, {
36
+ args: () =>
37
+ [
38
+ this.trackFragmentIndexLoader.status,
39
+ this.initSegmentsLoader.status,
40
+ this.seekTask.status,
41
+ this.fetchSeekTask.status,
42
+ this.videoAssetTask.status,
43
+ this.paintTask.status,
44
+ ] as const,
45
+ task: async () => {
46
+ await this.trackFragmentIndexLoader.taskComplete;
47
+ await this.initSegmentsLoader.taskComplete;
48
+ await this.seekTask.taskComplete;
49
+ await this.fetchSeekTask.taskComplete;
50
+ await this.videoAssetTask.taskComplete;
51
+ await this.paintTask.taskComplete;
52
+ },
53
+ });
54
+
55
+ paintTask = new Task(this, {
56
+ args: () => [this.videoAssetTask.value, this.desiredSeekTimeMs] as const,
57
+ task: async (
58
+ [videoAsset, seekToMs],
59
+ {
60
+ signal:
61
+ _signal /** Aborting seeks is counter-productive. It is better to drop seeks. */,
62
+ },
63
+ ) => {
64
+ if (!videoAsset) {
65
+ return;
66
+ }
67
+ if (this.#decoderLock) {
68
+ return;
69
+ }
70
+ try {
71
+ this.#decoderLock = true;
72
+ const frame = await videoAsset.seekToTime(seekToMs / 1000);
73
+
74
+ if (!this.canvasElement) {
75
+ return;
76
+ }
77
+ const ctx = this.canvasElement.getContext("2d");
78
+ if (!(frame && ctx)) {
79
+ return;
80
+ }
81
+
82
+ this.canvasElement.width = frame?.codedWidth;
83
+ this.canvasElement.height = frame?.codedHeight;
84
+
85
+ ctx.drawImage(
86
+ frame,
87
+ 0,
88
+ 0,
89
+ this.canvasElement.width,
90
+ this.canvasElement.height,
91
+ );
92
+
93
+ return seekToMs;
94
+ } catch (error) {
95
+ console.trace("Unexpected error while seeking video", error);
96
+ // As a practical matter, we should probably just re-create the VideoAsset if decoding fails.
97
+ // The decoder is probably in an invalid state anyway.
98
+ } finally {
99
+ this.#decoderLock = false;
100
+ }
101
+ },
102
+ });
103
+ }