@editframe/elements 0.11.0-beta.8 → 0.12.0-beta.1
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.
- package/dist/EF_FRAMEGEN.d.ts +8 -15
- package/dist/elements/EFCaptions.d.ts +50 -6
- package/dist/elements/EFMedia.d.ts +1 -1
- package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
- package/dist/elements/EFTimegroup.d.ts +23 -2
- package/dist/elements/EFWaveform.d.ts +17 -13
- package/dist/elements/src/EF_FRAMEGEN.js +24 -26
- package/dist/elements/src/elements/EFCaptions.js +295 -42
- package/dist/elements/src/elements/EFImage.js +3 -13
- package/dist/elements/src/elements/EFMedia.js +0 -5
- package/dist/elements/src/elements/EFTemporal.js +13 -10
- package/dist/elements/src/elements/EFTimegroup.js +37 -12
- package/dist/elements/src/elements/EFVideo.js +7 -7
- package/dist/elements/src/elements/EFWaveform.js +262 -149
- package/dist/elements/src/gui/ContextMixin.js +36 -7
- package/dist/elements/src/gui/EFFilmstrip.js +16 -3
- package/dist/elements/src/gui/EFScrubber.js +142 -0
- package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
- package/dist/elements/src/gui/EFTogglePlay.js +14 -14
- package/dist/elements/src/gui/EFWorkbench.js +1 -24
- package/dist/elements/src/gui/TWMixin.css.js +1 -1
- package/dist/elements/src/index.js +8 -1
- package/dist/gui/ContextMixin.d.ts +2 -1
- package/dist/gui/EFScrubber.d.ts +23 -0
- package/dist/gui/EFTimeDisplay.d.ts +17 -0
- package/dist/gui/EFTogglePlay.d.ts +1 -1
- package/dist/gui/EFWorkbench.d.ts +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/style.css +6 -801
- package/package.json +2 -2
- package/src/elements/EFCaptions.browsertest.ts +6 -6
- package/src/elements/EFCaptions.ts +325 -56
- package/src/elements/EFImage.browsertest.ts +4 -17
- package/src/elements/EFImage.ts +4 -14
- package/src/elements/EFMedia.browsertest.ts +8 -19
- package/src/elements/EFMedia.ts +1 -6
- package/src/elements/EFTemporal.browsertest.ts +14 -0
- package/src/elements/EFTemporal.ts +14 -0
- package/src/elements/EFTimegroup.browsertest.ts +37 -0
- package/src/elements/EFTimegroup.ts +42 -17
- package/src/elements/EFVideo.ts +7 -8
- package/src/elements/EFWaveform.ts +349 -319
- package/src/gui/ContextMixin.browsertest.ts +28 -2
- package/src/gui/ContextMixin.ts +41 -9
- package/src/gui/EFFilmstrip.ts +16 -3
- package/src/gui/EFScrubber.ts +145 -0
- package/src/gui/EFTimeDisplay.ts +81 -0
- package/src/gui/EFTogglePlay.ts +21 -21
- package/src/gui/EFWorkbench.ts +3 -36
|
@@ -57,58 +57,47 @@ describe("EFMedia", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
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
60
|
test("determines fragmentIndexPath", () => {
|
|
72
61
|
const id = v4();
|
|
73
62
|
const element = document.createElement("test-media");
|
|
74
|
-
element.setAttribute("asset-id",
|
|
63
|
+
element.setAttribute("asset-id", id);
|
|
75
64
|
expect(element.fragmentIndexPath()).toBe(
|
|
76
|
-
`https://editframe.dev/api/v1/isobmff_files/${id}
|
|
65
|
+
`https://editframe.dev/api/v1/isobmff_files/${id}/index`,
|
|
77
66
|
);
|
|
78
67
|
});
|
|
79
68
|
|
|
80
69
|
test("determines fragmentTrackPath", () => {
|
|
81
70
|
const id = v4();
|
|
82
71
|
const element = document.createElement("test-media");
|
|
83
|
-
element.setAttribute("asset-id",
|
|
72
|
+
element.setAttribute("asset-id", id);
|
|
84
73
|
expect(element.fragmentTrackPath("1")).toBe(
|
|
85
|
-
`https://editframe.dev/api/v1/isobmff_tracks/${id}
|
|
74
|
+
`https://editframe.dev/api/v1/isobmff_tracks/${id}/1`,
|
|
86
75
|
);
|
|
87
76
|
});
|
|
88
77
|
|
|
89
78
|
test("honors apiHost in fragmentIndexPath", () => {
|
|
90
79
|
const id = v4();
|
|
91
80
|
const element = document.createElement("test-media");
|
|
92
|
-
element.setAttribute("asset-id",
|
|
81
|
+
element.setAttribute("asset-id", id);
|
|
93
82
|
const preview = document.createElement("ef-preview");
|
|
94
83
|
preview.appendChild(element);
|
|
95
84
|
preview.apiHost = "test://";
|
|
96
85
|
document.body.appendChild(preview);
|
|
97
86
|
expect(element.fragmentIndexPath()).toBe(
|
|
98
|
-
`test:///api/v1/isobmff_files/${id}
|
|
87
|
+
`test:///api/v1/isobmff_files/${id}/index`,
|
|
99
88
|
);
|
|
100
89
|
});
|
|
101
90
|
|
|
102
91
|
test("honors apiHost in fragmentTrackPath", () => {
|
|
103
92
|
const id = v4();
|
|
104
93
|
const element = document.createElement("test-media");
|
|
105
|
-
element.setAttribute("asset-id",
|
|
94
|
+
element.setAttribute("asset-id", id);
|
|
106
95
|
const preview = document.createElement("ef-preview");
|
|
107
96
|
preview.appendChild(element);
|
|
108
97
|
preview.apiHost = "test://";
|
|
109
98
|
document.body.appendChild(preview);
|
|
110
99
|
expect(element.fragmentTrackPath("1")).toBe(
|
|
111
|
-
`test:///api/v1/isobmff_tracks/${id}
|
|
100
|
+
`test:///api/v1/isobmff_tracks/${id}/1`,
|
|
112
101
|
);
|
|
113
102
|
});
|
|
114
103
|
});
|
package/src/elements/EFMedia.ts
CHANGED
|
@@ -57,11 +57,6 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
57
57
|
#assetId: string | null = null;
|
|
58
58
|
@property({ type: String, attribute: "asset-id", reflect: true })
|
|
59
59
|
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
60
|
this.#assetId = value;
|
|
66
61
|
}
|
|
67
62
|
|
|
@@ -104,7 +99,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
104
99
|
},
|
|
105
100
|
});
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
public initSegmentsLoader = new Task(this, {
|
|
108
103
|
autoRun: EF_INTERACTIVE,
|
|
109
104
|
args: () =>
|
|
110
105
|
[this.trackFragmentIndexLoader.value, this.src, this.fetch] as const,
|
|
@@ -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.
|
|
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("
|
|
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,
|
package/src/elements/EFVideo.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { html, css } from "lit";
|
|
2
1
|
import { Task } from "@lit/task";
|
|
3
|
-
import {
|
|
2
|
+
import { css, html } from "lit";
|
|
4
3
|
import { customElement } from "lit/decorators.js";
|
|
4
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
5
|
|
|
6
|
-
import { EFMedia } from "./EFMedia.ts";
|
|
7
6
|
import { TWMixin } from "../gui/TWMixin.ts";
|
|
7
|
+
import { EFMedia } from "./EFMedia.ts";
|
|
8
8
|
|
|
9
9
|
@customElement("ef-video")
|
|
10
10
|
export class EFVideo extends TWMixin(EFMedia) {
|
|
@@ -13,15 +13,14 @@ export class EFVideo extends TWMixin(EFMedia) {
|
|
|
13
13
|
:host {
|
|
14
14
|
display: block;
|
|
15
15
|
}
|
|
16
|
+
canvas {
|
|
17
|
+
all: inherit;
|
|
18
|
+
}
|
|
16
19
|
`,
|
|
17
20
|
];
|
|
18
21
|
canvasRef = createRef<HTMLCanvasElement>();
|
|
19
|
-
|
|
20
22
|
render() {
|
|
21
|
-
return html` <canvas
|
|
22
|
-
class="h-full w-full object-fill"
|
|
23
|
-
${ref(this.canvasRef)}
|
|
24
|
-
></canvas>`;
|
|
23
|
+
return html` <canvas ${ref(this.canvasRef)}></canvas>`;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
get canvasElement() {
|