@editframe/elements 0.11.0-beta.9 → 0.12.0-beta.10

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 (51) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +8 -15
  2. package/dist/assets/src/MP4File.js +73 -20
  3. package/dist/elements/EFCaptions.d.ts +50 -6
  4. package/dist/elements/EFMedia.d.ts +1 -2
  5. package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
  6. package/dist/elements/EFTimegroup.d.ts +23 -2
  7. package/dist/elements/EFWaveform.d.ts +15 -11
  8. package/dist/elements/src/EF_FRAMEGEN.js +24 -26
  9. package/dist/elements/src/elements/EFCaptions.js +295 -42
  10. package/dist/elements/src/elements/EFImage.js +0 -6
  11. package/dist/elements/src/elements/EFMedia.js +70 -18
  12. package/dist/elements/src/elements/EFTemporal.js +13 -10
  13. package/dist/elements/src/elements/EFTimegroup.js +37 -12
  14. package/dist/elements/src/elements/EFVideo.js +1 -4
  15. package/dist/elements/src/elements/EFWaveform.js +250 -143
  16. package/dist/elements/src/gui/ContextMixin.js +44 -11
  17. package/dist/elements/src/gui/EFPreview.js +3 -1
  18. package/dist/elements/src/gui/EFScrubber.js +142 -0
  19. package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
  20. package/dist/elements/src/gui/EFTogglePlay.js +11 -19
  21. package/dist/elements/src/gui/EFWorkbench.js +1 -24
  22. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  23. package/dist/elements/src/index.js +8 -1
  24. package/dist/gui/ContextMixin.d.ts +2 -1
  25. package/dist/gui/EFScrubber.d.ts +23 -0
  26. package/dist/gui/EFTimeDisplay.d.ts +17 -0
  27. package/dist/gui/EFTogglePlay.d.ts +0 -2
  28. package/dist/gui/EFWorkbench.d.ts +0 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/style.css +6 -801
  31. package/package.json +2 -2
  32. package/src/elements/EFCaptions.browsertest.ts +6 -6
  33. package/src/elements/EFCaptions.ts +325 -56
  34. package/src/elements/EFImage.browsertest.ts +4 -17
  35. package/src/elements/EFImage.ts +0 -6
  36. package/src/elements/EFMedia.browsertest.ts +10 -19
  37. package/src/elements/EFMedia.ts +87 -20
  38. package/src/elements/EFTemporal.browsertest.ts +14 -0
  39. package/src/elements/EFTemporal.ts +14 -0
  40. package/src/elements/EFTimegroup.browsertest.ts +37 -0
  41. package/src/elements/EFTimegroup.ts +42 -17
  42. package/src/elements/EFVideo.ts +1 -4
  43. package/src/elements/EFWaveform.ts +339 -314
  44. package/src/gui/ContextMixin.browsertest.ts +28 -2
  45. package/src/gui/ContextMixin.ts +52 -14
  46. package/src/gui/EFPreview.ts +4 -2
  47. package/src/gui/EFScrubber.ts +145 -0
  48. package/src/gui/EFTimeDisplay.ts +81 -0
  49. package/src/gui/EFTogglePlay.ts +19 -25
  50. package/src/gui/EFWorkbench.ts +3 -36
  51. package/dist/elements/src/elements/util.js +0 -11
@@ -14,9 +14,8 @@ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
14
14
  import { EF_RENDERING } from "../EF_RENDERING.ts";
15
15
  import { apiHostContext } from "../gui/apiHostContext.ts";
16
16
  import { EFSourceMixin } from "./EFSourceMixin.ts";
17
- import { EFTemporal } from "./EFTemporal.ts";
17
+ import { EFTemporal, isEFTemporal } from "./EFTemporal.ts";
18
18
  import { FetchMixin } from "./FetchMixin.ts";
19
- import { getStartTimeMs } from "./util.ts";
20
19
 
21
20
  const log = debug("ef:elements:EFMedia");
22
21
 
@@ -57,11 +56,6 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
57
56
  #assetId: string | null = null;
58
57
  @property({ type: String, attribute: "asset-id", reflect: true })
59
58
  set assetId(value: string | null) {
60
- if (!value?.match(/^.{8}-.{4}-.{4}-.{4}-.{12}:.*$/)) {
61
- throw new Error(
62
- "EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
63
- );
64
- }
65
59
  this.#assetId = value;
66
60
  }
67
61
 
@@ -104,7 +98,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
104
98
  },
105
99
  });
106
100
 
107
- protected initSegmentsLoader = new Task(this, {
101
+ public initSegmentsLoader = new Task(this, {
108
102
  autoRun: EF_INTERACTIVE,
109
103
  args: () =>
110
104
  [this.trackFragmentIndexLoader.value, this.src, this.fetch] as const,
@@ -115,10 +109,10 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
115
109
  return await Promise.all(
116
110
  Object.entries(fragmentIndex).map(async ([trackId, track]) => {
117
111
  const start = track.initSegment.offset;
118
- const end = track.initSegment.offset + track.initSegment.size - 1;
112
+ const end = track.initSegment.offset + track.initSegment.size;
119
113
  const response = await fetch(this.fragmentTrackPath(trackId), {
120
114
  signal,
121
- headers: { Range: `bytes=${start}-${end}` },
115
+ headers: { Range: `bytes=${start}-${end - 1}` },
122
116
  });
123
117
  const buffer =
124
118
  (await response.arrayBuffer()) as MP4Box.MP4ArrayBuffer;
@@ -225,7 +219,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
225
219
 
226
220
  const response = await fetch(this.fragmentTrackPath(trackId), {
227
221
  signal,
228
- headers: { Range: `bytes=${start}-${end}` },
222
+ headers: { Range: `bytes=${start}-${end - 1}` },
229
223
  });
230
224
 
231
225
  if (nextSegment) {
@@ -233,7 +227,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
233
227
  const nextEnd = nextSegment.offset + nextSegment.size;
234
228
  fetch(this.fragmentTrackPath(trackId), {
235
229
  signal,
236
- headers: { Range: `bytes=${nextStart}-${nextEnd}` },
230
+ headers: { Range: `bytes=${nextStart}-${nextEnd - 1}` },
237
231
  })
238
232
  .then(() => {
239
233
  log("Prefetched next segment");
@@ -302,6 +296,83 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
302
296
  if (changedProperties.has("ownCurrentTimeMs")) {
303
297
  this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
304
298
  }
299
+ // TODO: this is copied straight from EFTimegroup.ts
300
+ // and should be refactored to be shared/reduce bad duplication of
301
+ // critical logic.
302
+ if (
303
+ changedProperties.has("currentTime") ||
304
+ changedProperties.has("ownCurrentTimeMs")
305
+ ) {
306
+ const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
307
+ if (
308
+ this.startTimeMs > timelineTimeMs ||
309
+ this.endTimeMs < timelineTimeMs
310
+ ) {
311
+ this.style.display = "none";
312
+ return;
313
+ }
314
+ this.style.display = "";
315
+
316
+ const animations = this.getAnimations({ subtree: true });
317
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
318
+ this.style.setProperty(
319
+ "--ef-transition--duration",
320
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`,
321
+ );
322
+ this.style.setProperty(
323
+ "--ef-transition-out-start",
324
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
325
+ );
326
+
327
+ for (const animation of animations) {
328
+ if (animation.playState === "running") {
329
+ animation.pause();
330
+ }
331
+ const effect = animation.effect;
332
+ if (!(effect && effect instanceof KeyframeEffect)) {
333
+ return;
334
+ }
335
+ const target = effect.target;
336
+ // TODO: better generalize work avoidance for temporal elements
337
+ if (!target) {
338
+ return;
339
+ }
340
+ if (target.closest("ef-video, ef-audio") !== this) {
341
+ return;
342
+ }
343
+
344
+ // Important to avoid going to the end of the animation
345
+ // or it will reset awkwardly.
346
+ if (isEFTemporal(target)) {
347
+ const timing = effect.getTiming();
348
+ const duration = Number(timing.duration) ?? 0;
349
+ const delay = Number(timing.delay);
350
+ const newTime = Math.floor(
351
+ Math.min(target.ownCurrentTimeMs, duration - 1 + delay),
352
+ );
353
+ if (Number.isNaN(newTime)) {
354
+ return;
355
+ }
356
+ animation.currentTime = newTime;
357
+ } else if (target) {
358
+ const nearestTimegroup = target.closest("ef-timegroup");
359
+ if (!nearestTimegroup) {
360
+ return;
361
+ }
362
+ const timing = effect.getTiming();
363
+ const duration = Number(timing.duration) ?? 0;
364
+ const delay = Number(timing.delay);
365
+ const newTime = Math.floor(
366
+ Math.min(nearestTimegroup.ownCurrentTimeMs, duration - 1 + delay),
367
+ );
368
+
369
+ if (Number.isNaN(newTime)) {
370
+ return;
371
+ }
372
+ animation.currentTime = newTime;
373
+ }
374
+ }
375
+ }
305
376
  }
306
377
 
307
378
  get hasOwnDuration() {
@@ -356,10 +427,6 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
356
427
  return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
357
428
  }
358
429
 
359
- get startTimeMs() {
360
- return getStartTimeMs(this);
361
- }
362
-
363
430
  #audioContext = new OfflineAudioContext(2, 48000 / 30, 48000);
364
431
 
365
432
  audioBufferTask = new Task(this, {
@@ -418,11 +485,11 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
418
485
 
419
486
  const start = audioTrackIndex.initSegment.offset;
420
487
  const end =
421
- audioTrackIndex.initSegment.offset + audioTrackIndex.initSegment.size - 1;
488
+ audioTrackIndex.initSegment.offset + audioTrackIndex.initSegment.size;
422
489
  const audioInitFragmentRequest = this.fetch(
423
490
  this.fragmentTrackPath(String(audioTrackId)),
424
491
  {
425
- headers: { Range: `bytes=${start}-${end}` },
492
+ headers: { Range: `bytes=${start}-${end - 1}` },
426
493
  },
427
494
  );
428
495
 
@@ -448,12 +515,12 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
448
515
  return;
449
516
  }
450
517
  const fragmentStart = firstFragment.offset;
451
- const fragmentEnd = lastFragment.offset + lastFragment.size - 1;
518
+ const fragmentEnd = lastFragment.offset + lastFragment.size;
452
519
 
453
520
  const audioFragmentRequest = this.fetch(
454
521
  this.fragmentTrackPath(String(audioTrackId)),
455
522
  {
456
- headers: { Range: `bytes=${fragmentStart}-${fragmentEnd}` },
523
+ headers: { Range: `bytes=${fragmentStart}-${fragmentEnd - 1}` },
457
524
  },
458
525
  );
459
526
 
@@ -63,3 +63,17 @@ describe("trimstart and trimend", () => {
63
63
  expect(element.trimEndMs).toBe(5_000);
64
64
  });
65
65
  });
66
+
67
+ describe("duration", () => {
68
+ test("duration is parsed correctly", () => {
69
+ const element = document.createElement("test-temporal");
70
+ element.setAttribute("duration", "10s");
71
+ expect(element.durationMs).toBe(10_000);
72
+ });
73
+
74
+ test("duration can be set directly on the element", () => {
75
+ const element = document.createElement("test-temporal");
76
+ element.duration = "10s";
77
+ expect(element.durationMs).toBe(10_000);
78
+ });
79
+ });
@@ -169,6 +169,14 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
169
169
  })
170
170
  private _durationMs?: number;
171
171
 
172
+ set duration(value: string | undefined) {
173
+ if (value !== undefined) {
174
+ this.setAttribute("duration", value);
175
+ } else {
176
+ this.removeAttribute("duration");
177
+ }
178
+ }
179
+
172
180
  private _trimStartMs = 0;
173
181
  @property({
174
182
  type: Number,
@@ -179,6 +187,9 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
179
187
  return this._trimStartMs;
180
188
  }
181
189
  public set trimStartMs(value: number) {
190
+ if (this._trimStartMs === value) {
191
+ return;
192
+ }
182
193
  this._trimStartMs = value;
183
194
  this.setAttribute(
184
195
  "trimstart",
@@ -203,6 +214,9 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
203
214
  return this._trimEndMs;
204
215
  }
205
216
  public set trimEndMs(value: number) {
217
+ if (this._trimEndMs === value) {
218
+ return;
219
+ }
206
220
  this._trimEndMs = value;
207
221
  this.setAttribute("trimend", durationConverter.toAttribute(value / 1000));
208
222
  }
@@ -8,7 +8,10 @@ import { assert, beforeEach, describe, test } from "vitest";
8
8
  import { EFTimegroup } from "./EFTimegroup.ts";
9
9
  import "./EFTimegroup.ts";
10
10
  import { customElement } from "lit/decorators/custom-element.js";
11
+ import { ContextMixin } from "../gui/ContextMixin.ts";
11
12
  import { EFTemporal } from "./EFTemporal.ts";
13
+ // Need workbench to make workbench wrapping occurs
14
+ import "../gui/EFWorkbench.ts";
12
15
 
13
16
  beforeEach(() => {
14
17
  for (let i = 0; i < localStorage.length; i++) {
@@ -21,6 +24,9 @@ beforeEach(() => {
21
24
  }
22
25
  });
23
26
 
27
+ @customElement("test-context")
28
+ class TestContext extends ContextMixin(LitElement) {}
29
+
24
30
  @customElement("test-temporal")
25
31
  class TestTemporal extends EFTemporal(LitElement) {
26
32
  get hasOwnDuration(): boolean {
@@ -31,6 +37,7 @@ class TestTemporal extends EFTemporal(LitElement) {
31
37
  declare global {
32
38
  interface HTMLElementTagNameMap {
33
39
  "test-temporal": TestTemporal;
40
+ "test-context": TestContext;
34
41
  }
35
42
  }
36
43
 
@@ -331,3 +338,33 @@ describe("setting currentTime", () => {
331
338
  assert.equal(b.ownCurrentTimeMs, 2_500);
332
339
  });
333
340
  });
341
+
342
+ describe("shouldWrapWithWorkbench", () => {
343
+ test.skip("should not wrap if EF_INTERACTIVE is false", () => {
344
+ // TODO: need a way to define EF_INTERACTIVE in a test
345
+ });
346
+
347
+ test("should wrap if root-most timegroup", () => {
348
+ const root = document.createElement("ef-timegroup");
349
+ const child = document.createElement("ef-timegroup");
350
+ root.appendChild(child);
351
+ assert.isTrue(child.shouldWrapWithWorkbench());
352
+ });
353
+
354
+ test("should not wrap if contained within a preview context", () => {
355
+ const timegorup = document.createElement("ef-timegroup");
356
+ const context = document.createElement("test-context");
357
+ context.append(timegorup);
358
+ assert.isFalse(timegorup.shouldWrapWithWorkbench());
359
+ });
360
+ });
361
+
362
+ describe("DOM nodes", () => {
363
+ test("can have mode and duration set as attributes", () => {
364
+ const timegroup = document.createElement("ef-timegroup");
365
+ timegroup.setAttribute("mode", "fixed");
366
+ timegroup.setAttribute("duration", "10s");
367
+ assert.equal(timegroup.mode, "fixed");
368
+ assert.equal(timegroup.durationMs, 10_000);
369
+ });
370
+ });
@@ -1,9 +1,12 @@
1
- import { LitElement, html, css, type PropertyValueMap } from "lit";
2
1
  import { provide } from "@lit/context";
3
2
  import { Task } from "@lit/task";
4
- import { customElement, property } from "lit/decorators.js";
5
3
  import debug from "debug";
4
+ import { LitElement, type PropertyValueMap, css, html } from "lit";
5
+ import { customElement, property } from "lit/decorators.js";
6
6
 
7
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
8
+ import { isContextMixin } from "../gui/ContextMixin.ts";
9
+ import { deepGetMediaElements } from "./EFMedia.ts";
7
10
  import {
8
11
  EFTemporal,
9
12
  isEFTemporal,
@@ -11,8 +14,6 @@ import {
11
14
  timegroupContext,
12
15
  } from "./EFTemporal.ts";
13
16
  import { TimegroupController } from "./TimegroupController.ts";
14
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
- import { deepGetMediaElements } from "./EFMedia.ts";
16
17
  import { durationConverter } from "./durationConverter.ts";
17
18
 
18
19
  const log = debug("ef:elements:EFTimegroup");
@@ -151,10 +152,16 @@ export class EFTimegroup extends EFTemporal(LitElement) {
151
152
  }
152
153
  }
153
154
 
155
+ /**
156
+ * Wait for all media elements to load their initial segments.
157
+ * Ideally we would only need the extracted index json data, but
158
+ * that caused issues with constructing audio data. We had negative durations
159
+ * in calculations and it was not clear why.
160
+ */
154
161
  async waitForMediaDurations() {
155
162
  return await Promise.all(
156
163
  deepGetMediaElements(this).map(
157
- (media) => media.trackFragmentIndexLoader.taskComplete,
164
+ (media) => media.initSegmentsLoader.taskComplete,
158
165
  ),
159
166
  );
160
167
  }
@@ -243,12 +250,34 @@ export class EFTimegroup extends EFTemporal(LitElement) {
243
250
  }
244
251
  }
245
252
 
253
+ get contextProvider() {
254
+ let parent = this.parentNode;
255
+ while (parent) {
256
+ if (isContextMixin(parent)) {
257
+ return parent;
258
+ }
259
+ parent = parent.parentNode;
260
+ }
261
+ return null;
262
+ }
263
+
264
+ /**
265
+ * Returns true if the timegroup should be wrapped with a workbench.
266
+ *
267
+ * A timegroup should be wrapped with a workbench if it is the root-most timegroup
268
+ * and EF_INTERACTIVE is true.
269
+ *
270
+ * If the timegroup is already wrappedin a context provider like ef-preview,
271
+ * it should NOT be wrapped in a workbench.
272
+ *
273
+ */
246
274
  shouldWrapWithWorkbench() {
247
275
  return (
248
276
  EF_INTERACTIVE &&
249
277
  this.closest("ef-timegroup") === this &&
278
+ this.closest("ef-preview") === null &&
250
279
  this.closest("ef-workbench") === null &&
251
- this.closest("ef-preview") === null
280
+ this.closest("test-context") === null
252
281
  );
253
282
  }
254
283
 
@@ -286,12 +315,8 @@ export class EFTimegroup extends EFTemporal(LitElement) {
286
315
  ) {
287
316
  await this.waitForMediaDurations();
288
317
 
289
- const durationMs = toMs - fromMs;
290
-
291
318
  await Promise.all(
292
319
  deepGetMediaElements(this).map(async (mediaElement) => {
293
- await mediaElement.trackFragmentIndexLoader.taskComplete;
294
-
295
320
  const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
296
321
  const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
297
322
  const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
@@ -304,19 +329,19 @@ export class EFTimegroup extends EFTemporal(LitElement) {
304
329
  throw new Error("Failed to fetch audio");
305
330
  }
306
331
 
307
- const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
308
- const ctxEndMs = Math.min(durationMs, mediaElement.endTimeMs - fromMs);
309
- const ctxDurationMs = ctxEndMs - ctxStartMs;
310
-
311
- const offset =
312
- Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
313
-
314
332
  const bufferSource = audioContext.createBufferSource();
315
333
  bufferSource.buffer = await audioContext.decodeAudioData(
316
334
  await audio.blob.arrayBuffer(),
317
335
  );
318
336
  bufferSource.connect(audioContext.destination);
319
337
 
338
+ const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
339
+ const ctxEndMs = mediaElement.endTimeMs - fromMs;
340
+ const ctxDurationMs = ctxEndMs - ctxStartMs;
341
+
342
+ const offset =
343
+ Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
344
+
320
345
  bufferSource.start(
321
346
  ctxStartMs / 1000,
322
347
  offset / 1000,
@@ -20,10 +20,7 @@ export class EFVideo extends TWMixin(EFMedia) {
20
20
  ];
21
21
  canvasRef = createRef<HTMLCanvasElement>();
22
22
  render() {
23
- return html` <canvas
24
- class="h-full w-full object-fill"
25
- ${ref(this.canvasRef)}
26
- ></canvas>`;
23
+ return html` <canvas ${ref(this.canvasRef)}></canvas>`;
27
24
  }
28
25
 
29
26
  get canvasElement() {