@editframe/elements 0.11.0-beta.1 → 0.11.0-beta.2

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.
@@ -1,8 +1,8 @@
1
- import { LitElement, PropertyValueMap } from 'lit';
2
1
  import { Task } from '@lit/task';
2
+ import { LitElement, PropertyValueMap } from 'lit';
3
3
  import { TrackFragmentIndex, TrackSegment } from '../../../assets/src/index.ts';
4
- import { MP4File } from '../../../assets/src/MP4File.ts';
5
4
  import { VideoAsset } from '../../../assets/src/EncodedAsset.ts';
5
+ import { MP4File } from '../../../assets/src/MP4File.ts';
6
6
  import type * as MP4Box from "mp4box";
7
7
  export declare const deepGetMediaElements: (element: Element, medias?: EFMedia[]) => EFMedia[];
8
8
  declare const EFMedia_base: (new (...args: any[]) => import('./EFSourceMixin.ts').EFSourceMixinInterface) & (new (...args: any[]) => import('./EFTemporal.ts').TemporalMixinInterface) & (new (...args: any[]) => import('./FetchMixin.ts').FetchMixinInterface) & typeof LitElement;
@@ -0,0 +1,10 @@
1
+ import { LitElement } from 'lit';
2
+ declare const TestTemporal_base: (new (...args: any[]) => import('./EFTemporal.ts').TemporalMixinInterface) & typeof LitElement;
3
+ declare class TestTemporal extends TestTemporal_base {
4
+ }
5
+ declare global {
6
+ interface HTMLElementTagNameMap {
7
+ "test-temporal": TestTemporal;
8
+ }
9
+ }
10
+ export {};
@@ -8,6 +8,16 @@ export declare class TemporalMixinInterface {
8
8
  get hasOwnDuration(): boolean;
9
9
  get trimStartMs(): number;
10
10
  get trimEndMs(): number;
11
+ set trimStartMs(value: number);
12
+ set trimEndMs(value: number);
13
+ set trimstart(value: string);
14
+ set trimend(value: string);
15
+ get sourceInMs(): number;
16
+ get sourceOutMs(): number;
17
+ set sourceInMs(value: number);
18
+ set sourceOutMs(value: number);
19
+ set sourcein(value: string);
20
+ set sourceout(value: string);
11
21
  get durationMs(): number;
12
22
  get startTimeMs(): number;
13
23
  get startTimeWithinParentMs(): number;
@@ -16,8 +26,6 @@ export declare class TemporalMixinInterface {
16
26
  get trimAdjustedOwnCurrentTimeMs(): number;
17
27
  set duration(value: string);
18
28
  get duration(): string;
19
- set trimstart(value: string);
20
- set trimend(value: string);
21
29
  parentTimegroup?: EFTimegroup;
22
30
  rootTimegroup?: EFTimegroup;
23
31
  frameTask: Task<readonly unknown[], unknown>;
@@ -10,3 +10,7 @@ export declare const imageDurationConverter: {
10
10
  fromAttribute: (value: string) => number;
11
11
  toAttribute: (value: number) => string;
12
12
  };
13
+ export declare const sourceDurationConverter: {
14
+ fromAttribute: (value: string) => number;
15
+ toAttribute: (value: number) => string;
16
+ };
@@ -1,18 +1,18 @@
1
- import { css, LitElement } from "lit";
2
- import { property, state } from "lit/decorators.js";
3
- import { deepArrayEquals } from "@lit/task/deep-equals.js";
4
- import { Task } from "@lit/task";
5
1
  import { consume } from "@lit/context";
2
+ import { Task } from "@lit/task";
3
+ import { deepArrayEquals } from "@lit/task/deep-equals.js";
6
4
  import debug from "debug";
7
- import { MP4File } from "../../../assets/src/MP4File.js";
5
+ import { css, LitElement } from "lit";
6
+ import { property, state } from "lit/decorators.js";
8
7
  import { VideoAsset } from "../../../assets/src/EncodedAsset.js";
8
+ import { MP4File } from "../../../assets/src/MP4File.js";
9
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
10
+ import { EF_RENDERING } from "../EF_RENDERING.js";
11
+ import { apiHostContext } from "../gui/apiHostContext.js";
12
+ import { EFSourceMixin } from "./EFSourceMixin.js";
9
13
  import { EFTemporal } from "./EFTemporal.js";
10
14
  import { FetchMixin } from "./FetchMixin.js";
11
- import { EFSourceMixin } from "./EFSourceMixin.js";
12
15
  import { getStartTimeMs } from "./util.js";
13
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
14
- import { apiHostContext } from "../gui/apiHostContext.js";
15
- import { EF_RENDERING } from "../EF_RENDERING.js";
16
16
  var __defProp = Object.defineProperty;
17
17
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
18
18
  var __decorateClass = (decorators, target, key, kind) => {
@@ -291,6 +291,18 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
291
291
  if (durations.length === 0) {
292
292
  return 0;
293
293
  }
294
+ if (this.sourceInMs && this.sourceOutMs && this.sourceOutMs > this.sourceInMs) {
295
+ return Math.max(this.sourceOutMs - this.sourceInMs);
296
+ }
297
+ if (this.sourceInMs) {
298
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs - this.sourceInMs;
299
+ }
300
+ if (this.sourceOutMs) {
301
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs - this.sourceOutMs;
302
+ }
303
+ if (this.sourceInMs && this.sourceOutMs) {
304
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs - this.sourceOutMs - this.sourceInMs;
305
+ }
294
306
  return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
295
307
  }
296
308
  get startTimeMs() {
@@ -298,8 +310,8 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
298
310
  }
299
311
  #audioContext;
300
312
  async fetchAudioSpanningTime(fromMs, toMs) {
301
- fromMs -= this.startTimeMs - this.trimStartMs;
302
- toMs -= this.startTimeMs - this.trimStartMs;
313
+ fromMs -= this.startTimeMs - this.trimStartMs - this.sourceInMs;
314
+ toMs -= this.startTimeMs - this.trimStartMs - this.sourceOutMs;
303
315
  await this.trackFragmentIndexLoader.taskComplete;
304
316
  const audioTrackId = this.defaultAudioTrackId;
305
317
  if (!audioTrackId) {
@@ -1,8 +1,8 @@
1
1
  import { createContext, consume } from "@lit/context";
2
2
  import { property, state } from "lit/decorators.js";
3
- import { durationConverter } from "./durationConverter.js";
4
3
  import { Task } from "@lit/task";
5
4
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
5
+ import { durationConverter } from "./durationConverter.js";
6
6
  var __defProp = Object.defineProperty;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __decorateClass = (decorators, target, key, kind) => {
@@ -118,9 +118,68 @@ const EFTemporal = (superClass) => {
118
118
  get trimStartMs() {
119
119
  return this._trimStartMs;
120
120
  }
121
+ set trimStartMs(value) {
122
+ this._trimStartMs = value;
123
+ this.setAttribute(
124
+ "trimstart",
125
+ durationConverter.toAttribute(value / 1e3)
126
+ );
127
+ }
128
+ set trimstart(value) {
129
+ if (value !== void 0) {
130
+ this.setAttribute("trimstart", value);
131
+ } else {
132
+ this.removeAttribute("trimstart");
133
+ }
134
+ }
121
135
  get trimEndMs() {
122
136
  return this._trimEndMs;
123
137
  }
138
+ set trimEndMs(value) {
139
+ this._trimEndMs = value;
140
+ this.setAttribute("trimend", durationConverter.toAttribute(value / 1e3));
141
+ }
142
+ set trimend(value) {
143
+ if (value !== void 0) {
144
+ this.setAttribute("trimend", value);
145
+ } else {
146
+ this.removeAttribute("trimend");
147
+ }
148
+ }
149
+ get sourceInMs() {
150
+ return this._sourceInMs;
151
+ }
152
+ set sourceInMs(value) {
153
+ this._sourceInMs = value;
154
+ value !== void 0 ? this.setAttribute(
155
+ "sourcein",
156
+ durationConverter.toAttribute(value / 1e3)
157
+ ) : this.removeAttribute("sourcein");
158
+ }
159
+ set sourcein(value) {
160
+ if (value !== void 0) {
161
+ this.setAttribute("sourcein", value);
162
+ } else {
163
+ this.removeAttribute("sourcein");
164
+ }
165
+ }
166
+ get sourceOutMs() {
167
+ return this._sourceOutMs;
168
+ }
169
+ set sourceOutMs(value) {
170
+ this._sourceOutMs = value;
171
+ value !== void 0 ? this.setAttribute(
172
+ "sourceout",
173
+ durationConverter.toAttribute(value / 1e3)
174
+ ) : this.removeAttribute("sourceout");
175
+ }
176
+ set sourceout(value) {
177
+ if (value !== void 0) {
178
+ this.setAttribute("sourceout", value);
179
+ } else {
180
+ this.removeAttribute("sourceout");
181
+ }
182
+ }
124
183
  get startOffsetMs() {
125
184
  return this._startOffsetMs;
126
185
  }
@@ -137,6 +196,12 @@ const EFTemporal = (superClass) => {
137
196
  // Defining this as a getter to a private property allows us to
138
197
  // override it classes that include this mixin.
139
198
  get durationMs() {
199
+ if (this.sourceInMs) {
200
+ return this._durationMs || this.parentTimegroup?.durationMs || 0 - this.sourceInMs;
201
+ }
202
+ if (this.sourceOutMs) {
203
+ return this._durationMs || this.parentTimegroup?.durationMs || 0 - this.sourceOutMs;
204
+ }
140
205
  return this._durationMs || this.parentTimegroup?.durationMs || 0;
141
206
  }
142
207
  get offsetMs() {
@@ -214,6 +279,15 @@ const EFTemporal = (superClass) => {
214
279
  */
215
280
  get trimAdjustedOwnCurrentTimeMs() {
216
281
  if (this.rootTimegroup) {
282
+ if (this.sourceInMs && this.sourceOutMs) {
283
+ return Math.min(
284
+ Math.max(
285
+ 0,
286
+ this.rootTimegroup.currentTimeMs - this.startTimeMs + this.trimStartMs + this.sourceInMs
287
+ ),
288
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs + this.sourceInMs
289
+ );
290
+ }
217
291
  return Math.min(
218
292
  Math.max(
219
293
  0,
@@ -249,14 +323,30 @@ const EFTemporal = (superClass) => {
249
323
  attribute: "trimstart",
250
324
  converter: durationConverter
251
325
  })
252
- ], TemporalMixinClass.prototype, "_trimStartMs", 2);
326
+ ], TemporalMixinClass.prototype, "trimStartMs", 1);
253
327
  __decorateClass([
254
328
  property({
255
329
  type: Number,
256
330
  attribute: "trimend",
257
331
  converter: durationConverter
258
332
  })
259
- ], TemporalMixinClass.prototype, "_trimEndMs", 2);
333
+ ], TemporalMixinClass.prototype, "trimEndMs", 1);
334
+ __decorateClass([
335
+ property({
336
+ type: Number,
337
+ attribute: "sourcein",
338
+ converter: durationConverter,
339
+ reflect: true
340
+ })
341
+ ], TemporalMixinClass.prototype, "sourceInMs", 1);
342
+ __decorateClass([
343
+ property({
344
+ type: Number,
345
+ attribute: "sourceout",
346
+ converter: durationConverter,
347
+ reflect: true
348
+ })
349
+ ], TemporalMixinClass.prototype, "sourceOutMs", 1);
260
350
  __decorateClass([
261
351
  property({
262
352
  type: Number,
@@ -1,19 +1,19 @@
1
+ import { consume } from "@lit/context";
1
2
  import { css, html, nothing, LitElement } from "lit";
2
3
  import { property, customElement, state, eventOptions } from "lit/decorators.js";
3
- import { consume } from "@lit/context";
4
- import { styleMap } from "lit/directives/style-map.js";
5
4
  import { createRef, ref } from "lit/directives/ref.js";
6
- import { EFImage } from "../elements/EFImage.js";
5
+ import { styleMap } from "lit/directives/style-map.js";
7
6
  import { EFAudio } from "../elements/EFAudio.js";
8
- import { EFVideo } from "../elements/EFVideo.js";
9
7
  import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.js";
10
- import { EFWaveform } from "../elements/EFWaveform.js";
8
+ import { EFImage } from "../elements/EFImage.js";
11
9
  import { EFTimegroup } from "../elements/EFTimegroup.js";
10
+ import { EFVideo } from "../elements/EFVideo.js";
11
+ import { EFWaveform } from "../elements/EFWaveform.js";
12
12
  import { TimegroupController } from "../elements/TimegroupController.js";
13
- import { TWMixin } from "./TWMixin.js";
14
13
  import { msToTimeCode } from "../msToTimeCode.js";
15
- import { focusedElementContext } from "./focusedElementContext.js";
14
+ import { TWMixin } from "./TWMixin.js";
16
15
  import { focusContext } from "./focusContext.js";
16
+ import { focusedElementContext } from "./focusedElementContext.js";
17
17
  import { playingContext, loopContext } from "./playingContext.js";
18
18
  var __defProp = Object.defineProperty;
19
19
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -76,14 +76,14 @@ class FilmstripItem extends TWMixin(LitElement) {
76
76
  get gutterStyles() {
77
77
  return {
78
78
  position: "relative",
79
- left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
80
- width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`
79
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs - this.element.sourceInMs)}px`,
80
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs + this.element.sourceOutMs + this.element.sourceInMs)}px`
81
81
  };
82
82
  }
83
83
  get trimPortionStyles() {
84
84
  return {
85
85
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
86
- left: `${this.pixelsPerMs * this.element.trimStartMs}px`
86
+ left: `${this.pixelsPerMs * (this.element.trimStartMs + this.element.sourceInMs)}px`
87
87
  };
88
88
  }
89
89
  render() {
@@ -1,9 +1,9 @@
1
- import { LitElement, nothing, TemplateResult, ReactiveController, PropertyValueMap } from 'lit';
2
- import { EFImage } from '../elements/EFImage.ts';
1
+ import { LitElement, PropertyValueMap, ReactiveController, TemplateResult, nothing } from 'lit';
3
2
  import { EFAudio } from '../elements/EFAudio.ts';
4
- import { EFVideo } from '../elements/EFVideo.ts';
5
- import { EFTimegroup } from '../elements/EFTimegroup.ts';
3
+ import { EFImage } from '../elements/EFImage.ts';
6
4
  import { TemporalMixinInterface } from '../elements/EFTemporal.ts';
5
+ import { EFTimegroup } from '../elements/EFTimegroup.ts';
6
+ import { EFVideo } from '../elements/EFVideo.ts';
7
7
  import { TimegroupController } from '../elements/TimegroupController.ts';
8
8
  import { FocusContext } from './focusContext.ts';
9
9
  declare class ElementFilmstripController implements ReactiveController {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.11.0-beta.1",
3
+ "version": "0.11.0-beta.2",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -20,7 +20,7 @@
20
20
  "author": "",
21
21
  "license": "UNLICENSED",
22
22
  "dependencies": {
23
- "@editframe/assets": "0.11.0-beta.1",
23
+ "@editframe/assets": "0.11.0-beta.2",
24
24
  "@lit/context": "^1.1.2",
25
25
  "@lit/task": "^1.0.1",
26
26
  "d3": "^7.9.0",
@@ -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,8 +394,8 @@ 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
365
- fromMs -= this.startTimeMs - this.trimStartMs;
366
- toMs -= this.startTimeMs - this.trimStartMs;
397
+ fromMs -= this.startTimeMs - this.trimStartMs - this.sourceInMs;
398
+ toMs -= this.startTimeMs - this.trimStartMs - this.sourceOutMs;
367
399
 
368
400
  await this.trackFragmentIndexLoader.taskComplete;
369
401
  const audioTrackId = this.defaultAudioTrackId;
@@ -0,0 +1,65 @@
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
+ });
@@ -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,104 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
158
169
  })
159
170
  private _durationMs?: number;
160
171
 
172
+ private _trimStartMs = 0;
161
173
  @property({
162
174
  type: Number,
163
175
  attribute: "trimstart",
164
176
  converter: durationConverter,
165
177
  })
166
- private _trimStartMs = 0;
167
178
  public get trimStartMs(): number {
168
179
  return this._trimStartMs;
169
180
  }
181
+ public set trimStartMs(value: number) {
182
+ this._trimStartMs = value;
183
+ this.setAttribute(
184
+ "trimstart",
185
+ durationConverter.toAttribute(value / 1000),
186
+ );
187
+ }
188
+ set trimstart(value: string | undefined) {
189
+ if (value !== undefined) {
190
+ this.setAttribute("trimstart", value);
191
+ } else {
192
+ this.removeAttribute("trimstart");
193
+ }
194
+ }
170
195
 
196
+ private _trimEndMs = 0;
171
197
  @property({
172
198
  type: Number,
173
199
  attribute: "trimend",
174
200
  converter: durationConverter,
175
201
  })
176
- private _trimEndMs = 0;
177
202
  public get trimEndMs(): number {
178
203
  return this._trimEndMs;
179
204
  }
205
+ public set trimEndMs(value: number) {
206
+ this._trimEndMs = value;
207
+ this.setAttribute("trimend", durationConverter.toAttribute(value / 1000));
208
+ }
209
+ set trimend(value: string | undefined) {
210
+ if (value !== undefined) {
211
+ this.setAttribute("trimend", value);
212
+ } else {
213
+ this.removeAttribute("trimend");
214
+ }
215
+ }
216
+
217
+ private _sourceInMs: number | undefined;
218
+ @property({
219
+ type: Number,
220
+ attribute: "sourcein",
221
+ converter: durationConverter,
222
+ reflect: true,
223
+ })
224
+ get sourceInMs(): number | undefined {
225
+ return this._sourceInMs;
226
+ }
227
+ set sourceInMs(value: number | undefined) {
228
+ this._sourceInMs = value;
229
+ value !== undefined
230
+ ? this.setAttribute(
231
+ "sourcein",
232
+ durationConverter.toAttribute(value / 1000),
233
+ )
234
+ : this.removeAttribute("sourcein");
235
+ }
236
+ set sourcein(value: string | undefined) {
237
+ if (value !== undefined) {
238
+ this.setAttribute("sourcein", value);
239
+ } else {
240
+ this.removeAttribute("sourcein");
241
+ }
242
+ }
243
+
244
+ private _sourceOutMs: number | undefined;
245
+ @property({
246
+ type: Number,
247
+ attribute: "sourceout",
248
+ converter: durationConverter,
249
+ reflect: true,
250
+ })
251
+ get sourceOutMs(): number | undefined {
252
+ return this._sourceOutMs;
253
+ }
254
+ set sourceOutMs(value: number | undefined) {
255
+ this._sourceOutMs = value;
256
+ value !== undefined
257
+ ? this.setAttribute(
258
+ "sourceout",
259
+ durationConverter.toAttribute(value / 1000),
260
+ )
261
+ : this.removeAttribute("sourceout");
262
+ }
263
+ set sourceout(value: string | undefined) {
264
+ if (value !== undefined) {
265
+ this.setAttribute("sourceout", value);
266
+ } else {
267
+ this.removeAttribute("sourceout");
268
+ }
269
+ }
180
270
 
181
271
  @property({
182
272
  type: Number,
@@ -207,6 +297,20 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
207
297
  // Defining this as a getter to a private property allows us to
208
298
  // override it classes that include this mixin.
209
299
  get durationMs() {
300
+ if (this.sourceInMs) {
301
+ return (
302
+ this._durationMs ||
303
+ this.parentTimegroup?.durationMs ||
304
+ 0 - this.sourceInMs
305
+ );
306
+ }
307
+ if (this.sourceOutMs) {
308
+ return (
309
+ this._durationMs ||
310
+ this.parentTimegroup?.durationMs ||
311
+ 0 - this.sourceOutMs
312
+ );
313
+ }
210
314
  return this._durationMs || this.parentTimegroup?.durationMs || 0;
211
315
  }
212
316
 
@@ -297,6 +401,21 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
297
401
  */
298
402
  get trimAdjustedOwnCurrentTimeMs() {
299
403
  if (this.rootTimegroup) {
404
+ if (this.sourceInMs && this.sourceOutMs) {
405
+ return Math.min(
406
+ Math.max(
407
+ 0,
408
+ this.rootTimegroup.currentTimeMs -
409
+ this.startTimeMs +
410
+ this.trimStartMs +
411
+ this.sourceInMs,
412
+ ),
413
+ this.durationMs +
414
+ Math.abs(this.startOffsetMs) +
415
+ this.trimStartMs +
416
+ this.sourceInMs,
417
+ );
418
+ }
300
419
  return Math.min(
301
420
  Math.max(
302
421
  0,
@@ -1,10 +1,10 @@
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";
@@ -24,3 +24,7 @@ export const trimDurationConverter = positiveDurationConverter(
24
24
  export const imageDurationConverter = positiveDurationConverter(
25
25
  "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
26
  );
27
+
28
+ export const sourceDurationConverter = positiveDurationConverter(
29
+ "Sourcein & sourceout must be a positive value in milliseconds or seconds (1s, 1000ms)",
30
+ );
@@ -1,37 +1,37 @@
1
+ import { consume } from "@lit/context";
1
2
  import {
2
3
  LitElement,
3
- html,
4
+ type PropertyValueMap,
5
+ type ReactiveController,
6
+ type TemplateResult,
4
7
  css,
8
+ html,
5
9
  nothing,
6
- type TemplateResult,
7
- type ReactiveController,
8
- type PropertyValueMap,
9
10
  } from "lit";
10
11
  import {
11
12
  customElement,
12
- property,
13
13
  eventOptions,
14
+ property,
14
15
  state,
15
16
  } from "lit/decorators.js";
16
- import { consume } from "@lit/context";
17
+ import { createRef, ref } from "lit/directives/ref.js";
17
18
  import { styleMap } from "lit/directives/style-map.js";
18
- import { ref, createRef } from "lit/directives/ref.js";
19
19
 
20
- import { EFImage } from "../elements/EFImage.ts";
21
20
  import { EFAudio } from "../elements/EFAudio.ts";
22
- import { EFVideo } from "../elements/EFVideo.ts";
23
21
  import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.ts";
24
- import { EFWaveform } from "../elements/EFWaveform.ts";
25
- import { EFTimegroup } from "../elements/EFTimegroup.ts";
22
+ import { EFImage } from "../elements/EFImage.ts";
26
23
  import type { TemporalMixinInterface } from "../elements/EFTemporal.ts";
24
+ import { EFTimegroup } from "../elements/EFTimegroup.ts";
25
+ import { EFVideo } from "../elements/EFVideo.ts";
26
+ import { EFWaveform } from "../elements/EFWaveform.ts";
27
27
  import { TimegroupController } from "../elements/TimegroupController.ts";
28
- import { TWMixin } from "./TWMixin.ts";
29
28
  import { msToTimeCode } from "../msToTimeCode.ts";
30
- import { focusedElementContext } from "./focusedElementContext.ts";
31
- import { type FocusContext, focusContext } from "./focusContext.ts";
32
- import { playingContext, loopContext } from "./playingContext.ts";
33
- import type { EFWorkbench } from "./EFWorkbench.ts";
34
29
  import type { EFPreview } from "./EFPreview.ts";
30
+ import type { EFWorkbench } from "./EFWorkbench.ts";
31
+ import { TWMixin } from "./TWMixin.ts";
32
+ import { type FocusContext, focusContext } from "./focusContext.ts";
33
+ import { focusedElementContext } from "./focusedElementContext.ts";
34
+ import { loopContext, playingContext } from "./playingContext.ts";
35
35
 
36
36
  class ElementFilmstripController implements ReactiveController {
37
37
  constructor(
@@ -89,15 +89,15 @@ class FilmstripItem extends TWMixin(LitElement) {
89
89
  get gutterStyles() {
90
90
  return {
91
91
  position: "relative",
92
- left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
93
- width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`,
92
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs - this.element.sourceInMs)}px`,
93
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs + this.element.sourceOutMs + this.element.sourceInMs)}px`,
94
94
  };
95
95
  }
96
96
 
97
97
  get trimPortionStyles() {
98
98
  return {
99
99
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
100
- left: `${this.pixelsPerMs * this.element.trimStartMs}px`,
100
+ left: `${this.pixelsPerMs * (this.element.trimStartMs + this.element.sourceInMs)}px`,
101
101
  };
102
102
  }
103
103