@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.11.0-beta.9",
3
+ "version": "0.12.0-beta.10",
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.9",
23
+ "@editframe/assets": "0.12.0-beta.10",
24
24
  "@lit/context": "^1.1.2",
25
25
  "@lit/task": "^1.0.1",
26
26
  "d3": "^7.9.0",
@@ -21,14 +21,14 @@ describe("EFCaptions", () => {
21
21
 
22
22
  const target = document.createElement("ef-video");
23
23
  target.setAttribute("id", id);
24
- target.assetId = "550e8400-e29b-41d4-a716-446655440000:example.mp4";
24
+ target.assetId = "550e8400-e29b-41d4-a716-446655440000";
25
25
  document.body.appendChild(target);
26
26
  const captions = document.createElement("ef-captions");
27
27
  captions.setAttribute("target", id);
28
28
  document.body.appendChild(captions);
29
29
  workbench.appendChild(captions);
30
30
  expect(captions.captionsPath()).toBe(
31
- "editframe://api/v1/caption_files/550e8400-e29b-41d4-a716-446655440000:example.mp4",
31
+ "editframe://api/v1/caption_files/550e8400-e29b-41d4-a716-446655440000",
32
32
  );
33
33
  });
34
34
  });
@@ -38,13 +38,13 @@ describe("EFCaptions", () => {
38
38
  const id = v4();
39
39
  const target = document.createElement("ef-video");
40
40
  target.setAttribute("id", id);
41
- target.assetId = `${id}:example.mp4`;
41
+ target.assetId = id;
42
42
  document.body.appendChild(target);
43
43
  const captions = document.createElement("ef-captions");
44
44
  captions.setAttribute("target", id);
45
45
  document.body.appendChild(captions);
46
46
  expect(captions.captionsPath()).toBe(
47
- `https://editframe.dev/api/v1/caption_files/${id}:example.mp4`,
47
+ `https://editframe.dev/api/v1/caption_files/${id}`,
48
48
  );
49
49
  });
50
50
 
@@ -54,7 +54,7 @@ describe("EFCaptions", () => {
54
54
  const id = v4();
55
55
  const target = document.createElement("ef-video");
56
56
  target.setAttribute("id", id);
57
- target.assetId = `${id}:example.mp4`;
57
+ target.assetId = id;
58
58
  document.body.appendChild(target);
59
59
  const captions = document.createElement("ef-captions");
60
60
  captions.setAttribute("target", id);
@@ -62,7 +62,7 @@ describe("EFCaptions", () => {
62
62
  document.body.appendChild(preview);
63
63
  preview.apiHost = "test://";
64
64
  expect(captions.captionsPath()).toBe(
65
- `test:///api/v1/caption_files/${id}:example.mp4`,
65
+ `test:///api/v1/caption_files/${id}`,
66
66
  );
67
67
  });
68
68
  });
@@ -1,48 +1,56 @@
1
- import { LitElement, type PropertyValueMap, html, css } from "lit";
2
1
  import { Task } from "@lit/task";
2
+ import { LitElement, type PropertyValueMap, css, html } from "lit";
3
3
  import { customElement, property } from "lit/decorators.js";
4
- import { EFVideo } from "./EFVideo.ts";
4
+ import type { GetISOBMFFFileTranscriptionResult } from "../../../api/src/index.ts";
5
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
6
+ import { EF_RENDERING } from "../EF_RENDERING.ts";
7
+ import { CrossUpdateController } from "./CrossUpdateController.ts";
5
8
  import { EFAudio } from "./EFAudio.ts";
9
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
6
10
  import { EFTemporal } from "./EFTemporal.ts";
7
- import { CrossUpdateController } from "./CrossUpdateController.ts";
11
+ import { EFVideo } from "./EFVideo.ts";
8
12
  import { FetchMixin } from "./FetchMixin.ts";
9
- import { EFSourceMixin } from "./EFSourceMixin.ts";
10
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
11
- import { EF_RENDERING } from "../EF_RENDERING.ts";
12
13
 
13
- interface Word {
14
+ interface WordSegment {
14
15
  text: string;
15
16
  start: number;
16
17
  end: number;
17
- confidence: number;
18
18
  }
19
19
 
20
20
  interface Segment {
21
21
  start: number;
22
22
  end: number;
23
23
  text: string;
24
- confidence: number;
25
- words: Word[];
26
24
  }
27
25
 
28
26
  interface Caption {
29
- text: string;
30
27
  segments: Segment[];
31
- language: string;
28
+ word_segments: WordSegment[];
32
29
  }
33
30
 
31
+ const stopWords = new Set(["", ".", "!", "?", ","]);
32
+
34
33
  @customElement("ef-captions-active-word")
35
34
  export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
36
35
  static styles = [
37
36
  css`
38
37
  :host {
39
38
  display: inline-block;
39
+ white-space: pre;
40
+ }
41
+ :host([hidden]) {
42
+ display: none;
40
43
  }
41
44
  `,
42
45
  ];
43
46
 
44
47
  render() {
45
- return html`${this.wordText}`;
48
+ if (stopWords.has(this.wordText)) {
49
+ this.hidden = true;
50
+ return undefined;
51
+ }
52
+ this.hidden = false;
53
+ return html` ${this.wordText.trim()} `;
46
54
  }
47
55
 
48
56
  @property({ type: Number, attribute: false })
@@ -54,6 +62,9 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
54
62
  @property({ type: String, attribute: false })
55
63
  wordText = "";
56
64
 
65
+ @property({ type: Boolean, reflect: true })
66
+ hidden = false;
67
+
57
68
  get startTimeMs() {
58
69
  return this.wordStartMs || 0;
59
70
  }
@@ -63,6 +74,137 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
63
74
  }
64
75
  }
65
76
 
77
+ @customElement("ef-captions-segment")
78
+ export class EFCaptionsSegment extends EFTemporal(LitElement) {
79
+ static styles = [
80
+ css`
81
+ :host {
82
+ display: block;
83
+ }
84
+ :host([hidden]) {
85
+ display: none;
86
+ }
87
+ `,
88
+ ];
89
+
90
+ render() {
91
+ if (stopWords.has(this.segmentText)) {
92
+ this.hidden = true;
93
+ return undefined;
94
+ }
95
+ this.hidden = false;
96
+ return html`${this.segmentText}`;
97
+ }
98
+
99
+ @property({ type: Number, attribute: false })
100
+ segmentStartMs = 0;
101
+
102
+ @property({ type: Number, attribute: false })
103
+ segmentEndMs = 0;
104
+
105
+ @property({ type: String, attribute: false })
106
+ segmentText = "";
107
+
108
+ @property({ type: Boolean, reflect: true })
109
+ hidden = false;
110
+
111
+ get startTimeMs() {
112
+ return this.segmentStartMs || 0;
113
+ }
114
+
115
+ get durationMs(): number {
116
+ return this.segmentEndMs - this.segmentStartMs;
117
+ }
118
+ }
119
+
120
+ @customElement("ef-captions-before-active-word")
121
+ export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
122
+ static styles = [
123
+ css`
124
+ :host {
125
+ display: inline-block;
126
+ white-space: pre;
127
+ }
128
+ :host([hidden]) {
129
+ display: none;
130
+ }
131
+ `,
132
+ ];
133
+
134
+ render() {
135
+ if (stopWords.has(this.segmentText)) {
136
+ this.hidden = true;
137
+ return undefined;
138
+ }
139
+ this.hidden = false;
140
+ return html` ${this.segmentText}`;
141
+ }
142
+
143
+ @property({ type: Boolean, reflect: true })
144
+ hidden = false;
145
+
146
+ @property({ type: String, attribute: false })
147
+ segmentText = "";
148
+
149
+ @property({ type: Number, attribute: false })
150
+ segmentStartMs = 0;
151
+
152
+ @property({ type: Number, attribute: false })
153
+ segmentEndMs = 0;
154
+
155
+ get startTimeMs() {
156
+ return this.segmentStartMs || 0;
157
+ }
158
+
159
+ get durationMs(): number {
160
+ return this.segmentEndMs - this.segmentStartMs;
161
+ }
162
+ }
163
+
164
+ @customElement("ef-captions-after-active-word")
165
+ export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
166
+ static styles = [
167
+ css`
168
+ :host {
169
+ display: inline-block;
170
+ white-space: pre;
171
+ }
172
+ :host([hidden]) {
173
+ display: none;
174
+ }
175
+ `,
176
+ ];
177
+
178
+ render() {
179
+ if (stopWords.has(this.segmentText)) {
180
+ this.hidden = true;
181
+ return undefined;
182
+ }
183
+ this.hidden = false;
184
+ return html`${this.segmentText} `;
185
+ }
186
+
187
+ @property({ type: Boolean, reflect: true })
188
+ hidden = false;
189
+
190
+ @property({ type: String, attribute: false })
191
+ segmentText = "";
192
+
193
+ @property({ type: Number, attribute: false })
194
+ segmentStartMs = 0;
195
+
196
+ @property({ type: Number, attribute: false })
197
+ segmentEndMs = 0;
198
+
199
+ get startTimeMs() {
200
+ return this.segmentStartMs || 0;
201
+ }
202
+
203
+ get durationMs(): number {
204
+ return this.segmentEndMs - this.segmentStartMs;
205
+ }
206
+ }
207
+
66
208
  @customElement("ef-captions")
67
209
  export class EFCaptions extends EFSourceMixin(
68
210
  EFTemporal(FetchMixin(LitElement)),
@@ -71,11 +213,24 @@ export class EFCaptions extends EFSourceMixin(
71
213
  static styles = [
72
214
  css`
73
215
  :host {
74
- display: block;
216
+ display: flex;
217
+ flex-wrap: wrap;
218
+ align-items: baseline;
219
+ width: fit-content;
220
+ }
221
+ ::slotted(*) {
222
+ margin: 0;
223
+ padding: 0;
75
224
  }
76
225
  `,
77
226
  ];
78
227
 
228
+ @property({ type: String, attribute: "display-mode", reflect: true })
229
+ displayMode: "word" | "segment" | "context" = "segment";
230
+
231
+ @property({ type: Number, attribute: "context-words", reflect: true })
232
+ contextWords = 3;
233
+
79
234
  @property({ type: String, attribute: "target", reflect: true })
80
235
  targetSelector = "";
81
236
 
@@ -87,6 +242,27 @@ export class EFCaptions extends EFSourceMixin(
87
242
  wordStyle = "";
88
243
 
89
244
  activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
245
+ segmentContainers = this.getElementsByTagName("ef-captions-segment");
246
+ beforeActiveWordContainers = this.getElementsByTagName(
247
+ "ef-captions-before-active-word",
248
+ );
249
+ afterActiveWordContainers = this.getElementsByTagName(
250
+ "ef-captions-after-active-word",
251
+ );
252
+
253
+ render() {
254
+ return html`<slot></slot>`;
255
+ }
256
+
257
+ transcriptionsPath() {
258
+ if (this.targetElement.assetId) {
259
+ if (EF_RENDERING()) {
260
+ return `editframe://api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
261
+ }
262
+ return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
263
+ }
264
+ return null;
265
+ }
90
266
 
91
267
  captionsPath() {
92
268
  if (this.targetElement.assetId) {
@@ -109,20 +285,71 @@ export class EFCaptions extends EFSourceMixin(
109
285
  },
110
286
  });
111
287
 
112
- private captionsDataTask = new Task(this, {
288
+ private transcriptionDataTask = new Task(this, {
113
289
  autoRun: EF_INTERACTIVE,
114
- args: () => [this.captionsPath(), this.fetch] as const,
115
- task: async ([captionsPath, fetch], { signal }) => {
116
- const response = await fetch(captionsPath, { signal });
290
+ args: () => [this.transcriptionsPath(), this.fetch] as const,
291
+ task: async ([transcriptionsPath, fetch], { signal }) => {
292
+ if (!transcriptionsPath) {
293
+ return null;
294
+ }
295
+ const response = await fetch(transcriptionsPath, { signal });
296
+ return response.json() as any as GetISOBMFFFileTranscriptionResult;
297
+ },
298
+ });
299
+
300
+ private transcriptionFragmentPath(
301
+ transcriptionId: string,
302
+ fragmentIndex: number,
303
+ ) {
304
+ return `${this.apiHost}/api/v1/transcriptions/${transcriptionId}/fragments/${fragmentIndex}`;
305
+ }
306
+
307
+ private fragmentIndexTask = new Task(this, {
308
+ autoRun: EF_INTERACTIVE,
309
+ args: () =>
310
+ [this.transcriptionDataTask.value, this.ownCurrentTimeMs] as const,
311
+ task: async ([transcription, ownCurrentTimeMs]) => {
312
+ if (!transcription) {
313
+ return null;
314
+ }
315
+ const fragmentIndex = Math.floor(
316
+ ownCurrentTimeMs / transcription.work_slice_ms,
317
+ );
318
+ return fragmentIndex;
319
+ },
320
+ });
321
+
322
+ private transcriptionFragmentDataTask = new Task(this, {
323
+ autoRun: EF_INTERACTIVE,
324
+ args: () =>
325
+ [
326
+ this.transcriptionDataTask.value,
327
+ this.fragmentIndexTask.value,
328
+ this.fetch,
329
+ ] as const,
330
+ task: async ([transcription, fragmentIndex, fetch], { signal }) => {
331
+ if (
332
+ transcription === null ||
333
+ transcription === undefined ||
334
+ fragmentIndex === null ||
335
+ fragmentIndex === undefined
336
+ ) {
337
+ return null;
338
+ }
339
+ const fragmentPath = this.transcriptionFragmentPath(
340
+ transcription.id,
341
+ fragmentIndex,
342
+ );
343
+ const response = await fetch(fragmentPath, { signal });
117
344
  return response.json() as any as Caption;
118
345
  },
119
346
  });
120
347
 
121
348
  frameTask = new Task(this, {
122
349
  autoRun: EF_INTERACTIVE,
123
- args: () => [this.captionsDataTask.status],
350
+ args: () => [this.transcriptionFragmentDataTask.status],
124
351
  task: async () => {
125
- await this.captionsDataTask.taskComplete;
352
+ await this.transcriptionFragmentDataTask.taskComplete;
126
353
  },
127
354
  });
128
355
 
@@ -133,51 +360,90 @@ export class EFCaptions extends EFSourceMixin(
133
360
  }
134
361
  }
135
362
 
136
- render() {
137
- return this.captionsDataTask.render({
138
- pending: () => html`<div>Generating captions data...</div>`,
139
- error: () => html`<div>🚫 Error generating captions data</div>`,
140
- complete: () => html`<slot></slot>`,
141
- });
142
- }
143
-
144
363
  protected updated(
145
364
  _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
146
365
  ): void {
147
- this.updateActiveWord();
366
+ this.updateTextContainers();
148
367
  }
149
368
 
150
- updateActiveWord() {
151
- const caption = this.captionsDataTask.value;
152
- if (!caption) {
369
+ updateTextContainers() {
370
+ const transcriptionFragment = this.transcriptionFragmentDataTask
371
+ .value as Caption;
372
+ if (!transcriptionFragment) {
153
373
  return;
154
374
  }
155
- const words: string[] = [];
156
- let startMs = 0;
157
- let endMs = 0;
158
- for (const segment of caption.segments) {
159
- if (
160
- this.targetElement.trimAdjustedOwnCurrentTimeMs >=
161
- segment.start * 1000 &&
162
- this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1000
163
- ) {
164
- for (const word of segment.words) {
165
- if (
166
- this.targetElement.trimAdjustedOwnCurrentTimeMs >=
167
- word.start * 1000 &&
168
- this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1000
169
- ) {
170
- words.push(word.text);
171
- startMs = word.start * 1000;
172
- endMs = word.end * 1000;
173
- }
174
- }
375
+
376
+ const currentTimeMs = this.targetElement.trimAdjustedOwnCurrentTimeMs;
377
+ const currentTimeSec = currentTimeMs / 1000;
378
+
379
+ // Find the current word from word_segments
380
+ const currentWord = transcriptionFragment.word_segments.find(
381
+ (word) => currentTimeSec >= word.start && currentTimeSec <= word.end,
382
+ );
383
+
384
+ // Find the current segment
385
+ const currentSegment = transcriptionFragment.segments.find(
386
+ (segment) =>
387
+ currentTimeSec >= segment.start && currentTimeSec <= segment.end,
388
+ );
389
+
390
+ for (const wordContainer of this.activeWordContainers) {
391
+ if (currentWord) {
392
+ wordContainer.wordText = currentWord.text;
393
+ wordContainer.wordStartMs = currentWord.start * 1000;
394
+ wordContainer.wordEndMs = currentWord.end * 1000;
395
+ }
396
+ }
397
+
398
+ for (const segmentContainer of this.segmentContainers) {
399
+ if (currentSegment) {
400
+ segmentContainer.segmentText = currentSegment.text;
401
+ segmentContainer.segmentStartMs = currentSegment.start * 1000;
402
+ segmentContainer.segmentEndMs = currentSegment.end * 1000;
175
403
  }
176
404
  }
177
- for (const container of Array.from(this.activeWordContainers)) {
178
- container.wordText = words.join(" ");
179
- container.wordStartMs = startMs;
180
- container.wordEndMs = endMs;
405
+
406
+ // Only process context if we have both a current word and segment
407
+ if (currentWord && currentSegment) {
408
+ // Find all word segments that fall within the current segment's time range
409
+ const segmentWords = transcriptionFragment.word_segments.filter(
410
+ (word) =>
411
+ word.start >= currentSegment.start && word.end <= currentSegment.end,
412
+ );
413
+
414
+ // Find the index of the current word within the segment's word segments
415
+ const currentWordIndex = segmentWords.findIndex(
416
+ (word) =>
417
+ word.start === currentWord.start && word.end === currentWord.end,
418
+ );
419
+
420
+ if (currentWordIndex !== -1) {
421
+ // Get words before the current word
422
+ const beforeWords = segmentWords
423
+ .slice(0, currentWordIndex)
424
+ .map((w) => w.text.trim())
425
+ .join(" ");
426
+
427
+ // Get words after the current word
428
+ const afterWords = segmentWords
429
+ .slice(currentWordIndex + 1)
430
+ .map((w) => w.text.trim())
431
+ .join(" ");
432
+
433
+ // Update before containers
434
+ for (const container of this.beforeActiveWordContainers) {
435
+ container.segmentText = beforeWords;
436
+ container.segmentStartMs = currentSegment.start * 1000;
437
+ container.segmentEndMs = currentWord.start * 1000;
438
+ }
439
+
440
+ // Update after containers
441
+ for (const container of this.afterActiveWordContainers) {
442
+ container.segmentText = afterWords;
443
+ container.segmentStartMs = currentWord.end * 1000;
444
+ container.segmentEndMs = currentSegment.end * 1000;
445
+ }
446
+ }
181
447
  }
182
448
  }
183
449
 
@@ -194,5 +460,8 @@ declare global {
194
460
  interface HTMLElementTagNameMap {
195
461
  "ef-captions": EFCaptions;
196
462
  "ef-captions-active-word": EFCaptionsActiveWord;
463
+ "ef-captions-segment": EFCaptionsSegment;
464
+ "ef-captions-before-active-word": EFCaptionsBeforeActiveWord;
465
+ "ef-captions-after-active-word": EFCaptionsAfterActiveWord;
197
466
  }
198
467
  }
@@ -26,23 +26,12 @@ describe("EFImage", () => {
26
26
  });
27
27
 
28
28
  describe("attribute: asset-id", () => {
29
- test("must match :id/basename", () => {
30
- const image = document.createElement("ef-image");
31
- expect(() => {
32
- image.assetId = "1234:example.mp4";
33
- }).toThrowError(
34
- new Error(
35
- "EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
36
- ),
37
- );
38
- });
39
-
40
29
  test("determines assetPath", () => {
41
30
  const id = v4();
42
31
  const image = document.createElement("ef-image");
43
- image.setAttribute("asset-id", `${id}:example.jpg`);
32
+ image.setAttribute("asset-id", id);
44
33
  expect(image.assetPath()).toBe(
45
- `https://editframe.dev/api/v1/image_files/${id}:example.jpg`,
34
+ `https://editframe.dev/api/v1/image_files/${id}`,
46
35
  );
47
36
  });
48
37
 
@@ -50,13 +39,11 @@ describe("EFImage", () => {
50
39
  const id = v4();
51
40
  const image = document.createElement("ef-image");
52
41
  const preview = document.createElement("ef-preview");
53
- image.setAttribute("asset-id", `${id}:example.jpg`);
42
+ image.setAttribute("asset-id", id);
54
43
  preview.appendChild(image);
55
44
  preview.apiHost = "test://";
56
45
  document.body.appendChild(preview);
57
- expect(image.assetPath()).toBe(
58
- `test:///api/v1/image_files/${id}:example.jpg`,
59
- );
46
+ expect(image.assetPath()).toBe(`test:///api/v1/image_files/${id}`);
60
47
  });
61
48
  });
62
49
  });
@@ -28,12 +28,6 @@ export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
28
28
  #assetId: string | null = null;
29
29
  @property({ type: String, attribute: "asset-id", reflect: true })
30
30
  set assetId(value: string | null) {
31
- if (!value?.match(/^.{8}-.{4}-.{4}-.{4}-.{12}:.*$/)) {
32
- console.error(`EFMedia ${value} is not valid asset-id`);
33
- throw new Error(
34
- "EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
35
- );
36
- }
37
31
  this.#assetId = value;
38
32
  }
39
33
 
@@ -4,6 +4,7 @@ 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 "./EFTimegroup.ts";
7
8
  import { createTestFragmentIndex } from "TEST/createTestFragmentIndex.ts";
8
9
  import { useMockWorker } from "TEST/useMockWorker.ts";
9
10
  import { http, HttpResponse } from "msw";
@@ -57,58 +58,47 @@ describe("EFMedia", () => {
57
58
  });
58
59
 
59
60
  describe("attribute: asset-id", () => {
60
- test("must match :id/basename", () => {
61
- const element = document.createElement("test-media");
62
- expect(() => {
63
- element.assetId = "1234:example.mp4";
64
- }).toThrowError(
65
- new Error(
66
- "EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
67
- ),
68
- );
69
- });
70
-
71
61
  test("determines fragmentIndexPath", () => {
72
62
  const id = v4();
73
63
  const element = document.createElement("test-media");
74
- element.setAttribute("asset-id", `${id}:example.mp4`);
64
+ element.setAttribute("asset-id", id);
75
65
  expect(element.fragmentIndexPath()).toBe(
76
- `https://editframe.dev/api/v1/isobmff_files/${id}:example.mp4/index`,
66
+ `https://editframe.dev/api/v1/isobmff_files/${id}/index`,
77
67
  );
78
68
  });
79
69
 
80
70
  test("determines fragmentTrackPath", () => {
81
71
  const id = v4();
82
72
  const element = document.createElement("test-media");
83
- element.setAttribute("asset-id", `${id}:example.mp4`);
73
+ element.setAttribute("asset-id", id);
84
74
  expect(element.fragmentTrackPath("1")).toBe(
85
- `https://editframe.dev/api/v1/isobmff_tracks/${id}:example.mp4/1`,
75
+ `https://editframe.dev/api/v1/isobmff_tracks/${id}/1`,
86
76
  );
87
77
  });
88
78
 
89
79
  test("honors apiHost in fragmentIndexPath", () => {
90
80
  const id = v4();
91
81
  const element = document.createElement("test-media");
92
- element.setAttribute("asset-id", `${id}:example.mp4`);
82
+ element.setAttribute("asset-id", id);
93
83
  const preview = document.createElement("ef-preview");
94
84
  preview.appendChild(element);
95
85
  preview.apiHost = "test://";
96
86
  document.body.appendChild(preview);
97
87
  expect(element.fragmentIndexPath()).toBe(
98
- `test:///api/v1/isobmff_files/${id}:example.mp4/index`,
88
+ `test:///api/v1/isobmff_files/${id}/index`,
99
89
  );
100
90
  });
101
91
 
102
92
  test("honors apiHost in fragmentTrackPath", () => {
103
93
  const id = v4();
104
94
  const element = document.createElement("test-media");
105
- element.setAttribute("asset-id", `${id}:example.mp4`);
95
+ element.setAttribute("asset-id", id);
106
96
  const preview = document.createElement("ef-preview");
107
97
  preview.appendChild(element);
108
98
  preview.apiHost = "test://";
109
99
  document.body.appendChild(preview);
110
100
  expect(element.fragmentTrackPath("1")).toBe(
111
- `test:///api/v1/isobmff_tracks/${id}:example.mp4/1`,
101
+ `test:///api/v1/isobmff_tracks/${id}/1`,
112
102
  );
113
103
  });
114
104
  });
@@ -302,6 +292,7 @@ describe("EFMedia", () => {
302
292
  expect(element.durationMs).toBe(4_000);
303
293
  expect(timegroup.durationMs).toBe(4_000);
304
294
  });
295
+
305
296
  test("Computes duration from track fragment index sourceout and sourcein", async () => {
306
297
  // Mock the request for the track fragment index, responds with a 10 second duration
307
298
  worker.use(