@editframe/elements 0.11.0-beta.1 → 0.11.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 (38) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +8 -15
  2. package/dist/elements/EFMedia.d.ts +2 -2
  3. package/dist/elements/EFTemporal.browsertest.d.ts +10 -0
  4. package/dist/elements/EFTemporal.d.ts +10 -2
  5. package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
  6. package/dist/elements/EFTimegroup.d.ts +12 -1
  7. package/dist/elements/EFWaveform.d.ts +3 -3
  8. package/dist/elements/durationConverter.d.ts +4 -0
  9. package/dist/elements/src/EF_FRAMEGEN.js +24 -26
  10. package/dist/elements/src/elements/EFImage.js +3 -7
  11. package/dist/elements/src/elements/EFMedia.js +27 -9
  12. package/dist/elements/src/elements/EFTemporal.js +100 -3
  13. package/dist/elements/src/elements/EFTimegroup.js +26 -5
  14. package/dist/elements/src/elements/EFVideo.js +7 -7
  15. package/dist/elements/src/elements/EFWaveform.js +14 -8
  16. package/dist/elements/src/gui/ContextMixin.js +20 -6
  17. package/dist/elements/src/gui/EFFilmstrip.js +28 -8
  18. package/dist/elements/src/gui/EFTogglePlay.js +38 -6
  19. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  20. package/dist/gui/ContextMixin.d.ts +1 -0
  21. package/dist/gui/EFFilmstrip.d.ts +4 -4
  22. package/dist/gui/EFTogglePlay.d.ts +4 -0
  23. package/dist/style.css +15 -4
  24. package/package.json +2 -2
  25. package/src/elements/EFImage.ts +4 -8
  26. package/src/elements/EFMedia.browsertest.ts +231 -2
  27. package/src/elements/EFMedia.ts +48 -9
  28. package/src/elements/EFTemporal.browsertest.ts +79 -0
  29. package/src/elements/EFTemporal.ts +133 -6
  30. package/src/elements/EFTimegroup.browsertest.ts +38 -1
  31. package/src/elements/EFTimegroup.ts +27 -6
  32. package/src/elements/EFVideo.ts +7 -8
  33. package/src/elements/EFWaveform.ts +14 -9
  34. package/src/elements/durationConverter.ts +4 -0
  35. package/src/gui/ContextMixin.browsertest.ts +28 -2
  36. package/src/gui/ContextMixin.ts +23 -7
  37. package/src/gui/EFFilmstrip.ts +37 -17
  38. package/src/gui/EFTogglePlay.ts +38 -7
@@ -1,9 +1,12 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "vitest";
2
- import { v4 } from "uuid";
3
1
  import { customElement } from "lit/decorators.js";
2
+ import { v4 } from "uuid";
3
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
4
4
  import { EFMedia } from "./EFMedia.ts";
5
5
  import "../gui/EFWorkbench.ts";
6
6
  import "../gui/EFPreview.ts";
7
+ import { createTestFragmentIndex } from "TEST/createTestFragmentIndex.ts";
8
+ import { useMockWorker } from "TEST/useMockWorker.ts";
9
+ import { http, HttpResponse } from "msw";
7
10
 
8
11
  @customElement("test-media")
9
12
  class TestMedia extends EFMedia {}
@@ -15,6 +18,8 @@ declare global {
15
18
  }
16
19
 
17
20
  describe("EFMedia", () => {
21
+ const worker = useMockWorker();
22
+
18
23
  test("should be defined", () => {
19
24
  const element = document.createElement("test-media");
20
25
  expect(element.tagName).toBe("TEST-MEDIA");
@@ -107,4 +112,228 @@ describe("EFMedia", () => {
107
112
  );
108
113
  });
109
114
  });
115
+
116
+ describe("calculating duration", () => {
117
+ test("Computes duration from track fragment index", async () => {
118
+ // Mock the request for the track fragment index, responds with a 10 second duration
119
+ worker.use(
120
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
121
+ return HttpResponse.json(
122
+ createTestFragmentIndex({
123
+ audio: { duration: 10_000 },
124
+ video: { duration: 10_000 },
125
+ }),
126
+ );
127
+ }),
128
+ );
129
+
130
+ const element = document.createElement("test-media");
131
+ element.src = "/assets/10s-bars.mp4";
132
+
133
+ const preview = document.createElement("ef-preview");
134
+ preview.appendChild(element);
135
+ document.body.appendChild(preview);
136
+
137
+ // Await the next tick to ensure the element has a chance to load the track fragment
138
+ await Promise.resolve();
139
+
140
+ await element.trackFragmentIndexLoader.taskComplete;
141
+
142
+ expect(element.durationMs).toBe(10_000);
143
+ });
144
+ test("Computes duration from track fragment index sourcein", async () => {
145
+ // Mock the request for the track fragment index, responds with a 10 second duration
146
+ worker.use(
147
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
148
+ return HttpResponse.json(
149
+ createTestFragmentIndex({
150
+ audio: { duration: 10_000 },
151
+ video: { duration: 10_000 },
152
+ }),
153
+ );
154
+ }),
155
+ );
156
+
157
+ const timegroup = document.createElement("ef-timegroup");
158
+ timegroup.mode = "sequence";
159
+ const element = document.createElement("test-media");
160
+ element.src = "/assets/10s-bars.mp4";
161
+ element.sourcein = "1s";
162
+
163
+ const preview = document.createElement("ef-preview");
164
+ timegroup.appendChild(element);
165
+ preview.appendChild(timegroup);
166
+ document.body.appendChild(preview);
167
+
168
+ // Await the next tick to ensure the element has a chance to load the track fragment
169
+ await Promise.resolve();
170
+
171
+ await element.trackFragmentIndexLoader.taskComplete;
172
+
173
+ expect(element.durationMs).toBe(9_000);
174
+ expect(timegroup.durationMs).toBe(9_000);
175
+ });
176
+ test("Computes duration from track fragment index sourcein", async () => {
177
+ // Mock the request for the track fragment index, responds with a 10 second duration
178
+ worker.use(
179
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
180
+ return HttpResponse.json(
181
+ createTestFragmentIndex({
182
+ audio: { duration: 10_000 },
183
+ video: { duration: 10_000 },
184
+ }),
185
+ );
186
+ }),
187
+ );
188
+
189
+ const timegroup = document.createElement("ef-timegroup");
190
+ timegroup.mode = "sequence";
191
+ const element = document.createElement("test-media");
192
+ element.src = "/assets/10s-bars.mp4";
193
+ element.sourcein = "6s";
194
+
195
+ const preview = document.createElement("ef-preview");
196
+ timegroup.appendChild(element);
197
+ preview.appendChild(timegroup);
198
+ document.body.appendChild(preview);
199
+
200
+ // Await the next tick to ensure the element has a chance to load the track fragment
201
+ await Promise.resolve();
202
+
203
+ await element.trackFragmentIndexLoader.taskComplete;
204
+
205
+ expect(element.durationMs).toBe(4_000);
206
+ expect(timegroup.durationMs).toBe(4_000);
207
+ });
208
+ test("Computes duration from track fragment index sourceout", async () => {
209
+ // Mock the request for the track fragment index, responds with a 10 second duration
210
+ worker.use(
211
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
212
+ return HttpResponse.json(
213
+ createTestFragmentIndex({
214
+ audio: { duration: 10_000 },
215
+ video: { duration: 10_000 },
216
+ }),
217
+ );
218
+ }),
219
+ );
220
+
221
+ const timegroup = document.createElement("ef-timegroup");
222
+ timegroup.mode = "sequence";
223
+ const element = document.createElement("test-media");
224
+ element.src = "/assets/10s-bars.mp4";
225
+ element.sourceout = "6s";
226
+
227
+ const preview = document.createElement("ef-preview");
228
+ timegroup.appendChild(element);
229
+ preview.appendChild(timegroup);
230
+ document.body.appendChild(preview);
231
+
232
+ // Await the next tick to ensure the element has a chance to load the track fragment
233
+ await Promise.resolve();
234
+
235
+ await element.trackFragmentIndexLoader.taskComplete;
236
+
237
+ expect(element.durationMs).toBe(4_000);
238
+ expect(timegroup.durationMs).toBe(4_000);
239
+ });
240
+ test("Computes duration from track fragment index sourceout", async () => {
241
+ // Mock the request for the track fragment index, responds with a 10 second duration
242
+ worker.use(
243
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
244
+ return HttpResponse.json(
245
+ createTestFragmentIndex({
246
+ audio: { duration: 10_000 },
247
+ video: { duration: 10_000 },
248
+ }),
249
+ );
250
+ }),
251
+ );
252
+
253
+ const timegroup = document.createElement("ef-timegroup");
254
+ timegroup.mode = "sequence";
255
+ const element = document.createElement("test-media");
256
+ element.src = "/assets/10s-bars.mp4";
257
+ element.sourceout = "5s";
258
+
259
+ const preview = document.createElement("ef-preview");
260
+ timegroup.appendChild(element);
261
+ preview.appendChild(timegroup);
262
+ document.body.appendChild(preview);
263
+
264
+ // Await the next tick to ensure the element has a chance to load the track fragment
265
+ await Promise.resolve();
266
+
267
+ await element.trackFragmentIndexLoader.taskComplete;
268
+
269
+ expect(element.durationMs).toBe(5_000);
270
+ expect(timegroup.durationMs).toBe(5_000);
271
+ });
272
+ test("Computes duration from track fragment index sourceout and sourcein", async () => {
273
+ // Mock the request for the track fragment index, responds with a 10 second duration
274
+ worker.use(
275
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
276
+ return HttpResponse.json(
277
+ createTestFragmentIndex({
278
+ audio: { duration: 10_000 },
279
+ video: { duration: 10_000 },
280
+ }),
281
+ );
282
+ }),
283
+ );
284
+
285
+ const timegroup = document.createElement("ef-timegroup");
286
+ timegroup.mode = "sequence";
287
+ const element = document.createElement("test-media");
288
+ element.src = "/assets/10s-bars.mp4";
289
+ element.sourcein = "1s";
290
+ element.sourceout = "5s";
291
+
292
+ const preview = document.createElement("ef-preview");
293
+ timegroup.appendChild(element);
294
+ preview.appendChild(timegroup);
295
+ document.body.appendChild(preview);
296
+
297
+ // Await the next tick to ensure the element has a chance to load the track fragment
298
+ await Promise.resolve();
299
+
300
+ await element.trackFragmentIndexLoader.taskComplete;
301
+
302
+ expect(element.durationMs).toBe(4_000);
303
+ expect(timegroup.durationMs).toBe(4_000);
304
+ });
305
+ test("Computes duration from track fragment index sourceout and sourcein", async () => {
306
+ // Mock the request for the track fragment index, responds with a 10 second duration
307
+ worker.use(
308
+ http.get("/@ef-track-fragment-index//assets/10s-bars.mp4", () => {
309
+ return HttpResponse.json(
310
+ createTestFragmentIndex({
311
+ audio: { duration: 10_000 },
312
+ video: { duration: 10_000 },
313
+ }),
314
+ );
315
+ }),
316
+ );
317
+
318
+ const timegroup = document.createElement("ef-timegroup");
319
+ timegroup.mode = "sequence";
320
+ const element = document.createElement("test-media");
321
+ element.src = "/assets/10s-bars.mp4";
322
+ element.sourcein = "9s";
323
+ element.sourceout = "10s";
324
+
325
+ const preview = document.createElement("ef-preview");
326
+ timegroup.appendChild(element);
327
+ preview.appendChild(timegroup);
328
+ document.body.appendChild(preview);
329
+
330
+ // Await the next tick to ensure the element has a chance to load the track fragment
331
+ await Promise.resolve();
332
+
333
+ await element.trackFragmentIndexLoader.taskComplete;
334
+
335
+ expect(element.durationMs).toBe(1_000);
336
+ expect(timegroup.durationMs).toBe(1_000);
337
+ });
338
+ });
110
339
  });
@@ -1,22 +1,22 @@
1
+ import { consume } from "@lit/context";
2
+ import { Task } from "@lit/task";
3
+ import { deepArrayEquals } from "@lit/task/deep-equals.js";
4
+ import debug from "debug";
1
5
  import { LitElement, type PropertyValueMap, css } from "lit";
2
6
  import { property, state } from "lit/decorators.js";
3
- import { deepArrayEquals } from "@lit/task/deep-equals.js";
4
- import { Task } from "@lit/task";
5
7
  import type * as MP4Box from "mp4box";
6
- import { consume } from "@lit/context";
7
- import debug from "debug";
8
8
 
9
9
  import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
10
10
 
11
- import { MP4File } from "@editframe/assets/MP4File.js";
12
11
  import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
12
+ import { MP4File } from "@editframe/assets/MP4File.js";
13
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
14
+ import { EF_RENDERING } from "../EF_RENDERING.ts";
15
+ import { apiHostContext } from "../gui/apiHostContext.ts";
16
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
13
17
  import { EFTemporal } from "./EFTemporal.ts";
14
18
  import { FetchMixin } from "./FetchMixin.ts";
15
- import { EFSourceMixin } from "./EFSourceMixin.ts";
16
19
  import { getStartTimeMs } from "./util.ts";
17
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
18
- import { apiHostContext } from "../gui/apiHostContext.ts";
19
- import { EF_RENDERING } from "../EF_RENDERING.ts";
20
20
 
21
21
  const log = debug("ef:elements:EFMedia");
22
22
 
@@ -321,6 +321,38 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
321
321
  if (durations.length === 0) {
322
322
  return 0;
323
323
  }
324
+ if (
325
+ this.sourceInMs &&
326
+ this.sourceOutMs &&
327
+ this.sourceOutMs > this.sourceInMs
328
+ ) {
329
+ return Math.max(this.sourceOutMs - this.sourceInMs);
330
+ }
331
+ if (this.sourceInMs) {
332
+ return (
333
+ Math.max(...durations) -
334
+ this.trimStartMs -
335
+ this.trimEndMs -
336
+ this.sourceInMs
337
+ );
338
+ }
339
+ if (this.sourceOutMs) {
340
+ return (
341
+ Math.max(...durations) -
342
+ this.trimStartMs -
343
+ this.trimEndMs -
344
+ this.sourceOutMs
345
+ );
346
+ }
347
+ if (this.sourceInMs && this.sourceOutMs) {
348
+ return (
349
+ Math.max(...durations) -
350
+ this.trimStartMs -
351
+ this.trimEndMs -
352
+ this.sourceOutMs -
353
+ this.sourceInMs
354
+ );
355
+ }
324
356
  return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
325
357
  }
326
358
 
@@ -362,6 +394,12 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
362
394
 
363
395
  async fetchAudioSpanningTime(fromMs: number, toMs: number) {
364
396
  // Adjust range for track's own time
397
+ if (this.sourceInMs) {
398
+ fromMs -= this.startTimeMs - this.trimStartMs - this.sourceInMs;
399
+ }
400
+ if (this.sourceOutMs) {
401
+ toMs -= this.startTimeMs - this.trimStartMs - this.sourceOutMs;
402
+ }
365
403
  fromMs -= this.startTimeMs - this.trimStartMs;
366
404
  toMs -= this.startTimeMs - this.trimStartMs;
367
405
 
@@ -371,6 +409,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
371
409
  log("No audio track found");
372
410
  return;
373
411
  }
412
+
374
413
  const audioTrackIndex = this.trackFragmentIndexLoader.value?.[audioTrackId];
375
414
  if (!audioTrackIndex) {
376
415
  log("No audio track found");
@@ -0,0 +1,79 @@
1
+ import { LitElement } from "lit";
2
+ import { customElement } from "lit/decorators/custom-element.js";
3
+ import { describe, expect, test } from "vitest";
4
+ import { EFTemporal } from "./EFTemporal.ts";
5
+
6
+ @customElement("test-temporal")
7
+ class TestTemporal extends EFTemporal(LitElement) {}
8
+
9
+ declare global {
10
+ interface HTMLElementTagNameMap {
11
+ "test-temporal": TestTemporal;
12
+ }
13
+ }
14
+
15
+ describe("sourcein and sourceout", () => {
16
+ test("sourcein and sourceout are parsed correctly", () => {
17
+ const element = document.createElement("test-temporal");
18
+ element.setAttribute("sourcein", "1s");
19
+ element.setAttribute("sourceout", "5s");
20
+ expect(element.sourceInMs).toBe(1_000);
21
+ expect(element.sourceOutMs).toBe(5_000);
22
+ });
23
+
24
+ test("sourcein and sourceout can be set directly on the element", () => {
25
+ const element = document.createElement("test-temporal");
26
+ element.sourcein = "1s";
27
+ element.sourceout = "5s";
28
+ expect(element.sourceInMs).toBe(1_000);
29
+ expect(element.sourceOutMs).toBe(5_000);
30
+ });
31
+
32
+ test("sourcein and sourceout are reflected correctly", () => {
33
+ const element = document.createElement("test-temporal");
34
+ element.sourceInMs = 1_000;
35
+ element.sourceOutMs = 5_000;
36
+ expect(element.getAttribute("sourcein")).toBe("1s");
37
+ expect(element.getAttribute("sourceout")).toBe("5s");
38
+ });
39
+ });
40
+
41
+ describe("trimstart and trimend", () => {
42
+ test("trimstart and trimend attributes are parsed correctly", () => {
43
+ const element = document.createElement("test-temporal");
44
+ element.setAttribute("trimstart", "1s");
45
+ element.setAttribute("trimend", "5s");
46
+ expect(element.trimStartMs).toBe(1_000);
47
+ expect(element.trimEndMs).toBe(5_000);
48
+ });
49
+
50
+ test("trimstart and trimend properties are reflected correctly", () => {
51
+ const element = document.createElement("test-temporal");
52
+ element.trimStartMs = 1_000;
53
+ element.trimEndMs = 5_000;
54
+ expect(element.getAttribute("trimstart")).toBe("1s");
55
+ expect(element.getAttribute("trimend")).toBe("5s");
56
+ });
57
+
58
+ test("trimstart and trimend can be set directly on the element", () => {
59
+ const element = document.createElement("test-temporal");
60
+ element.trimstart = "1s";
61
+ element.trimend = "5s";
62
+ expect(element.trimStartMs).toBe(1_000);
63
+ expect(element.trimEndMs).toBe(5_000);
64
+ });
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
+ });
@@ -1,11 +1,11 @@
1
- import type { LitElement, ReactiveController } from "lit";
2
1
  import { consume, createContext } from "@lit/context";
2
+ import type { LitElement, ReactiveController } from "lit";
3
3
  import { property, state } from "lit/decorators.js";
4
4
  import type { EFTimegroup } from "./EFTimegroup.ts";
5
5
 
6
- import { durationConverter } from "./durationConverter.ts";
7
6
  import { Task } from "@lit/task";
8
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
8
+ import { durationConverter } from "./durationConverter.ts";
9
9
 
10
10
  export const timegroupContext = createContext<EFTimegroup>(
11
11
  Symbol("timeGroupContext"),
@@ -15,6 +15,19 @@ export declare class TemporalMixinInterface {
15
15
  get hasOwnDuration(): boolean;
16
16
  get trimStartMs(): number;
17
17
  get trimEndMs(): number;
18
+ set trimStartMs(value: number);
19
+ set trimEndMs(value: number);
20
+ set trimstart(value: string);
21
+ set trimend(value: string);
22
+
23
+ get sourceInMs(): number;
24
+ get sourceOutMs(): number;
25
+
26
+ set sourceInMs(value: number);
27
+ set sourceOutMs(value: number);
28
+ set sourcein(value: string);
29
+ set sourceout(value: string);
30
+
18
31
  get durationMs(): number;
19
32
  get startTimeMs(): number;
20
33
  get startTimeWithinParentMs(): number;
@@ -24,8 +37,6 @@ export declare class TemporalMixinInterface {
24
37
 
25
38
  set duration(value: string);
26
39
  get duration(): string;
27
- set trimstart(value: string);
28
- set trimend(value: string);
29
40
 
30
41
  parentTimegroup?: EFTimegroup;
31
42
  rootTimegroup?: EFTimegroup;
@@ -158,25 +169,112 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
158
169
  })
159
170
  private _durationMs?: number;
160
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
+
180
+ private _trimStartMs = 0;
161
181
  @property({
162
182
  type: Number,
163
183
  attribute: "trimstart",
164
184
  converter: durationConverter,
165
185
  })
166
- private _trimStartMs = 0;
167
186
  public get trimStartMs(): number {
168
187
  return this._trimStartMs;
169
188
  }
189
+ public set trimStartMs(value: number) {
190
+ this._trimStartMs = value;
191
+ this.setAttribute(
192
+ "trimstart",
193
+ durationConverter.toAttribute(value / 1000),
194
+ );
195
+ }
196
+ set trimstart(value: string | undefined) {
197
+ if (value !== undefined) {
198
+ this.setAttribute("trimstart", value);
199
+ } else {
200
+ this.removeAttribute("trimstart");
201
+ }
202
+ }
170
203
 
204
+ private _trimEndMs = 0;
171
205
  @property({
172
206
  type: Number,
173
207
  attribute: "trimend",
174
208
  converter: durationConverter,
175
209
  })
176
- private _trimEndMs = 0;
177
210
  public get trimEndMs(): number {
178
211
  return this._trimEndMs;
179
212
  }
213
+ public set trimEndMs(value: number) {
214
+ this._trimEndMs = value;
215
+ this.setAttribute("trimend", durationConverter.toAttribute(value / 1000));
216
+ }
217
+ set trimend(value: string | undefined) {
218
+ if (value !== undefined) {
219
+ this.setAttribute("trimend", value);
220
+ } else {
221
+ this.removeAttribute("trimend");
222
+ }
223
+ }
224
+
225
+ private _sourceInMs: number | undefined;
226
+ @property({
227
+ type: Number,
228
+ attribute: "sourcein",
229
+ converter: durationConverter,
230
+ reflect: true,
231
+ })
232
+ get sourceInMs(): number | undefined {
233
+ return this._sourceInMs;
234
+ }
235
+ set sourceInMs(value: number | undefined) {
236
+ this._sourceInMs = value;
237
+ value !== undefined
238
+ ? this.setAttribute(
239
+ "sourcein",
240
+ durationConverter.toAttribute(value / 1000),
241
+ )
242
+ : this.removeAttribute("sourcein");
243
+ }
244
+ set sourcein(value: string | undefined) {
245
+ if (value !== undefined) {
246
+ this.setAttribute("sourcein", value);
247
+ } else {
248
+ this.removeAttribute("sourcein");
249
+ }
250
+ }
251
+
252
+ private _sourceOutMs: number | undefined;
253
+ @property({
254
+ type: Number,
255
+ attribute: "sourceout",
256
+ converter: durationConverter,
257
+ reflect: true,
258
+ })
259
+ get sourceOutMs(): number | undefined {
260
+ return this._sourceOutMs;
261
+ }
262
+ set sourceOutMs(value: number | undefined) {
263
+ this._sourceOutMs = value;
264
+ value !== undefined
265
+ ? this.setAttribute(
266
+ "sourceout",
267
+ durationConverter.toAttribute(value / 1000),
268
+ )
269
+ : this.removeAttribute("sourceout");
270
+ }
271
+ set sourceout(value: string | undefined) {
272
+ if (value !== undefined) {
273
+ this.setAttribute("sourceout", value);
274
+ } else {
275
+ this.removeAttribute("sourceout");
276
+ }
277
+ }
180
278
 
181
279
  @property({
182
280
  type: Number,
@@ -207,6 +305,20 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
207
305
  // Defining this as a getter to a private property allows us to
208
306
  // override it classes that include this mixin.
209
307
  get durationMs() {
308
+ if (this.sourceInMs) {
309
+ return (
310
+ this._durationMs ||
311
+ this.parentTimegroup?.durationMs ||
312
+ 0 - this.sourceInMs
313
+ );
314
+ }
315
+ if (this.sourceOutMs) {
316
+ return (
317
+ this._durationMs ||
318
+ this.parentTimegroup?.durationMs ||
319
+ 0 - this.sourceOutMs
320
+ );
321
+ }
210
322
  return this._durationMs || this.parentTimegroup?.durationMs || 0;
211
323
  }
212
324
 
@@ -297,6 +409,21 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
297
409
  */
298
410
  get trimAdjustedOwnCurrentTimeMs() {
299
411
  if (this.rootTimegroup) {
412
+ if (this.sourceInMs && this.sourceOutMs) {
413
+ return Math.min(
414
+ Math.max(
415
+ 0,
416
+ this.rootTimegroup.currentTimeMs -
417
+ this.startTimeMs +
418
+ this.trimStartMs +
419
+ this.sourceInMs,
420
+ ),
421
+ this.durationMs +
422
+ Math.abs(this.startOffsetMs) +
423
+ this.trimStartMs +
424
+ this.sourceInMs,
425
+ );
426
+ }
300
427
  return Math.min(
301
428
  Math.max(
302
429
  0,
@@ -1,14 +1,17 @@
1
- import { describe, test, assert, beforeEach } from "vitest";
2
1
  import {
3
2
  LitElement,
4
3
  type TemplateResult,
5
4
  html,
6
5
  render as litRender,
7
6
  } from "lit";
7
+ 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
+ });