@editframe/elements 0.7.0-beta.9 → 0.8.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 +44 -0
- package/dist/EF_INTERACTIVE.d.ts +1 -0
- package/dist/assets/dist/EncodedAsset.js +560 -0
- package/dist/assets/dist/MP4File.js +170 -0
- package/dist/assets/dist/memoize.js +14 -0
- package/dist/elements/CrossUpdateController.d.ts +9 -0
- package/dist/elements/EFAudio.d.ts +10 -0
- package/dist/elements/EFCaptions.d.ts +38 -0
- package/dist/elements/EFImage.d.ts +14 -0
- package/dist/elements/EFMedia.d.ts +61 -0
- package/dist/elements/EFSourceMixin.d.ts +12 -0
- package/dist/elements/EFTemporal.d.ts +38 -0
- package/dist/elements/EFTimegroup.browsertest.d.ts +12 -0
- package/dist/elements/EFTimegroup.d.ts +39 -0
- package/dist/elements/EFVideo.d.ts +14 -0
- package/dist/elements/EFWaveform.d.ts +30 -0
- package/dist/elements/FetchMixin.d.ts +8 -0
- package/dist/elements/TimegroupController.d.ts +14 -0
- package/dist/elements/durationConverter.d.ts +4 -0
- package/dist/elements/parseTimeToMs.d.ts +1 -0
- package/{src/EF_FRAMEGEN.ts → dist/elements/src/EF_FRAMEGEN.js} +35 -115
- package/dist/elements/src/EF_INTERACTIVE.js +7 -0
- package/dist/elements/src/elements/CrossUpdateController.js +16 -0
- package/dist/elements/src/elements/EFAudio.js +54 -0
- package/dist/elements/src/elements/EFCaptions.js +166 -0
- package/dist/elements/src/elements/EFImage.js +80 -0
- package/dist/elements/src/elements/EFMedia.js +339 -0
- package/dist/elements/src/elements/EFSourceMixin.js +55 -0
- package/dist/elements/src/elements/EFTemporal.js +234 -0
- package/dist/elements/src/elements/EFTimegroup.js +355 -0
- package/dist/elements/src/elements/EFVideo.js +110 -0
- package/dist/elements/src/elements/EFWaveform.js +226 -0
- package/dist/elements/src/elements/FetchMixin.js +28 -0
- package/dist/elements/src/elements/TimegroupController.js +20 -0
- package/dist/elements/src/elements/durationConverter.js +8 -0
- package/dist/elements/src/elements/parseTimeToMs.js +12 -0
- package/dist/elements/src/elements/util.js +11 -0
- package/dist/elements/src/gui/ContextMixin.js +234 -0
- package/dist/elements/src/gui/EFFilmstrip.js +729 -0
- package/dist/elements/src/gui/EFPreview.js +45 -0
- package/dist/elements/src/gui/EFWorkbench.js +128 -0
- package/dist/elements/src/gui/TWMixin.css.js +4 -0
- package/dist/elements/src/gui/TWMixin.js +36 -0
- package/dist/elements/src/gui/apiHostContext.js +5 -0
- package/dist/elements/src/gui/fetchContext.js +5 -0
- package/dist/elements/src/gui/focusContext.js +5 -0
- package/dist/elements/src/gui/focusedElementContext.js +7 -0
- package/dist/elements/src/gui/playingContext.js +5 -0
- package/dist/elements/src/index.js +27 -0
- package/dist/elements/src/msToTimeCode.js +15 -0
- package/dist/elements/util.d.ts +4 -0
- package/dist/gui/ContextMixin.d.ts +23 -0
- package/dist/gui/EFFilmstrip.d.ts +144 -0
- package/dist/gui/EFPreview.d.ts +27 -0
- package/dist/gui/EFWorkbench.d.ts +34 -0
- package/dist/gui/TWMixin.d.ts +3 -0
- package/dist/gui/apiHostContext.d.ts +3 -0
- package/dist/gui/fetchContext.d.ts +3 -0
- package/dist/gui/focusContext.d.ts +6 -0
- package/dist/gui/focusedElementContext.d.ts +3 -0
- package/dist/gui/playingContext.d.ts +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/msToTimeCode.d.ts +1 -0
- package/dist/style.css +800 -0
- package/package.json +6 -9
- package/src/elements/EFAudio.ts +1 -1
- package/src/elements/EFCaptions.ts +9 -9
- package/src/elements/EFImage.ts +3 -3
- package/src/elements/EFMedia.ts +11 -8
- package/src/elements/EFSourceMixin.ts +1 -1
- package/src/elements/EFTemporal.ts +42 -5
- package/src/elements/EFTimegroup.browsertest.ts +3 -3
- package/src/elements/EFTimegroup.ts +9 -6
- package/src/elements/EFVideo.ts +2 -2
- package/src/elements/EFWaveform.ts +6 -6
- package/src/elements/FetchMixin.ts +5 -3
- package/src/elements/TimegroupController.ts +1 -1
- package/src/elements/durationConverter.ts +1 -1
- package/src/elements/util.ts +1 -1
- package/src/gui/ContextMixin.ts +254 -0
- package/src/gui/EFFilmstrip.ts +41 -150
- package/src/gui/EFPreview.ts +39 -0
- package/src/gui/EFWorkbench.ts +7 -105
- package/src/gui/TWMixin.ts +10 -3
- package/src/gui/apiHostContext.ts +3 -0
- package/src/gui/fetchContext.ts +5 -0
- package/src/gui/focusContext.ts +7 -0
- package/src/gui/focusedElementContext.ts +5 -0
- package/src/gui/playingContext.ts +3 -0
- package/CHANGELOG.md +0 -7
- package/postcss.config.cjs +0 -12
- package/src/EF_INTERACTIVE.ts +0 -2
- package/src/elements.css +0 -22
- package/src/index.ts +0 -33
- package/tailwind.config.ts +0 -10
- package/tsconfig.json +0 -4
- package/vite.config.ts +0 -8
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-beta.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"import": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
},
|
|
11
|
-
"require": {
|
|
12
|
-
"default": "./dist/packages/elements/src/index.cjs",
|
|
13
|
-
"types": "./dist/packages/elements/src/index.d.ts"
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/elements/src/index.js"
|
|
14
10
|
}
|
|
15
11
|
},
|
|
16
12
|
"./styles.css": "./dist/style.css"
|
|
@@ -24,7 +20,7 @@
|
|
|
24
20
|
"author": "",
|
|
25
21
|
"license": "UNLICENSED",
|
|
26
22
|
"dependencies": {
|
|
27
|
-
"@editframe/assets": "0.
|
|
23
|
+
"@editframe/assets": "0.8.0-beta.1",
|
|
28
24
|
"@lit/context": "^1.1.2",
|
|
29
25
|
"@lit/task": "^1.0.1",
|
|
30
26
|
"d3": "^7.9.0",
|
|
@@ -35,9 +31,10 @@
|
|
|
35
31
|
"devDependencies": {
|
|
36
32
|
"@types/d3": "^7.4.3",
|
|
37
33
|
"@types/dom-webcodecs": "^0.1.11",
|
|
38
|
-
"@types/node": "^20.14.
|
|
34
|
+
"@types/node": "^20.14.13",
|
|
39
35
|
"autoprefixer": "^10.4.19",
|
|
40
36
|
"rollup-plugin-tsconfig-paths": "^1.5.2",
|
|
37
|
+
"typescript": "^5.5.4",
|
|
41
38
|
"vite-plugin-dts": "^3.9.1",
|
|
42
39
|
"vite-tsconfig-paths": "^4.3.2"
|
|
43
40
|
}
|
package/src/elements/EFAudio.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html } from "lit";
|
|
2
2
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
3
3
|
import { customElement, property } from "lit/decorators.js";
|
|
4
|
-
import { EFMedia } from "./EFMedia";
|
|
4
|
+
import { EFMedia } from "./EFMedia.ts";
|
|
5
5
|
import { Task } from "@lit/task";
|
|
6
6
|
|
|
7
7
|
@customElement("ef-audio")
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { EFAudio } from "./EFAudio";
|
|
2
1
|
import { LitElement, type PropertyValueMap, html, css } from "lit";
|
|
3
2
|
import { Task } from "@lit/task";
|
|
4
3
|
import { customElement, property } from "lit/decorators.js";
|
|
5
|
-
import { EFVideo } from "./EFVideo";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { EFVideo } from "./EFVideo.ts";
|
|
5
|
+
import { EFAudio } from "./EFAudio.ts";
|
|
6
|
+
import { EFTemporal } from "./EFTemporal.ts";
|
|
7
|
+
import { CrossUpdateController } from "./CrossUpdateController.ts";
|
|
8
|
+
import { FetchMixin } from "./FetchMixin.ts";
|
|
9
|
+
import { EFSourceMixin } from "./EFSourceMixin.ts";
|
|
10
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
11
11
|
|
|
12
12
|
interface Word {
|
|
13
13
|
text: string;
|
|
@@ -93,8 +93,8 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
93
93
|
|
|
94
94
|
protected md5SumLoader = new Task(this, {
|
|
95
95
|
autoRun: false,
|
|
96
|
-
args: () => [this.target] as const,
|
|
97
|
-
task: async ([], { signal }) => {
|
|
96
|
+
args: () => [this.target, this.fetch] as const,
|
|
97
|
+
task: async ([_target, fetch], { signal }) => {
|
|
98
98
|
const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
|
|
99
99
|
const response = await fetch(md5Path, { method: "HEAD", signal });
|
|
100
100
|
return response.headers.get("etag") ?? undefined;
|
package/src/elements/EFImage.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Task } from "@lit/task";
|
|
|
2
2
|
import { LitElement, html, css } from "lit";
|
|
3
3
|
import { customElement } from "lit/decorators.js";
|
|
4
4
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
|
-
import { FetchMixin } from "./FetchMixin";
|
|
6
|
-
import { EFSourceMixin } from "./EFSourceMixin";
|
|
7
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
5
|
+
import { FetchMixin } from "./FetchMixin.ts";
|
|
6
|
+
import { EFSourceMixin } from "./EFSourceMixin.ts";
|
|
7
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
8
8
|
|
|
9
9
|
@customElement("ef-image")
|
|
10
10
|
export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
|
package/src/elements/EFMedia.ts
CHANGED
|
@@ -8,14 +8,14 @@ import debug from "debug";
|
|
|
8
8
|
|
|
9
9
|
import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
|
|
10
10
|
|
|
11
|
-
import { MP4File } from "
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { FetchMixin } from "./FetchMixin";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
11
|
+
import { MP4File } from "@editframe/assets/MP4File.js";
|
|
12
|
+
import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
|
|
13
|
+
import { EFTemporal } from "./EFTemporal.ts";
|
|
14
|
+
import { FetchMixin } from "./FetchMixin.ts";
|
|
15
|
+
import { EFSourceMixin } from "./EFSourceMixin.ts";
|
|
16
|
+
import { getStartTimeMs } from "./util.ts";
|
|
17
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
18
|
+
import { apiHostContext } from "../gui/apiHostContext.ts";
|
|
19
19
|
|
|
20
20
|
const log = debug("ef:elements:EFMedia");
|
|
21
21
|
|
|
@@ -70,6 +70,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
70
70
|
public trackFragmentIndexLoader = new Task(this, {
|
|
71
71
|
args: () => [this.fragmentIndexPath(), this.fetch] as const,
|
|
72
72
|
task: async ([fragmentIndexPath, fetch], { signal }) => {
|
|
73
|
+
if (this.src === "") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
73
76
|
const response = await fetch(fragmentIndexPath, { signal });
|
|
74
77
|
return (await response.json()) as Record<number, TrackFragmentIndex>;
|
|
75
78
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { consume } from "@lit/context";
|
|
2
2
|
import type { LitElement } from "lit";
|
|
3
|
-
import { apiHostContext } from "../gui/EFWorkbench";
|
|
4
3
|
import { state } from "lit/decorators/state.js";
|
|
5
4
|
import { Task } from "@lit/task";
|
|
6
5
|
import { property } from "lit/decorators/property.js";
|
|
6
|
+
import { apiHostContext } from "../gui/apiHostContext.ts";
|
|
7
7
|
|
|
8
8
|
export declare class EFSourceMixinInterface {
|
|
9
9
|
productionSrc(): string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { LitElement, ReactiveController } from "lit";
|
|
2
2
|
import { consume, createContext } from "@lit/context";
|
|
3
3
|
import { property, state } from "lit/decorators.js";
|
|
4
|
-
import type { EFTimegroup } from "./EFTimegroup";
|
|
4
|
+
import type { EFTimegroup } from "./EFTimegroup.ts";
|
|
5
5
|
|
|
6
|
-
import { durationConverter } from "./durationConverter";
|
|
6
|
+
import { durationConverter } from "./durationConverter.ts";
|
|
7
7
|
import { Task } from "@lit/task";
|
|
8
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
8
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
9
9
|
|
|
10
10
|
export const timegroupContext = createContext<EFTimegroup>(
|
|
11
11
|
Symbol("timeGroupContext"),
|
|
@@ -59,10 +59,23 @@ export const deepGetElementsWithFrameTasks = (
|
|
|
59
59
|
return elements;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
let temporalCache: Map<Element, TemporalMixinInterface[]>;
|
|
63
|
+
const resetTemporalCache = () => {
|
|
64
|
+
temporalCache = new Map();
|
|
65
|
+
if (typeof requestAnimationFrame !== "undefined") {
|
|
66
|
+
requestAnimationFrame(resetTemporalCache);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
resetTemporalCache();
|
|
70
|
+
|
|
62
71
|
export const shallowGetTemporalElements = (
|
|
63
72
|
element: Element,
|
|
64
73
|
temporals: TemporalMixinInterface[] = [],
|
|
65
74
|
) => {
|
|
75
|
+
const cachedResult = temporalCache.get(element);
|
|
76
|
+
if (cachedResult) {
|
|
77
|
+
return cachedResult;
|
|
78
|
+
}
|
|
66
79
|
for (const child of element.children) {
|
|
67
80
|
if (isEFTemporal(child)) {
|
|
68
81
|
temporals.push(child);
|
|
@@ -70,6 +83,7 @@ export const shallowGetTemporalElements = (
|
|
|
70
83
|
shallowGetTemporalElements(child, temporals);
|
|
71
84
|
}
|
|
72
85
|
}
|
|
86
|
+
temporalCache.set(element, temporals);
|
|
73
87
|
return temporals;
|
|
74
88
|
};
|
|
75
89
|
|
|
@@ -92,6 +106,15 @@ export class OwnCurrentTimeController implements ReactiveController {
|
|
|
92
106
|
|
|
93
107
|
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
94
108
|
|
|
109
|
+
let startTimeMsCache = new WeakMap<Element, number>();
|
|
110
|
+
const resetStartTimeMsCache = () => {
|
|
111
|
+
startTimeMsCache = new WeakMap();
|
|
112
|
+
if (typeof requestAnimationFrame !== "undefined") {
|
|
113
|
+
requestAnimationFrame(resetStartTimeMsCache);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
resetStartTimeMsCache();
|
|
117
|
+
|
|
95
118
|
export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
96
119
|
superClass: T,
|
|
97
120
|
) => {
|
|
@@ -172,27 +195,41 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
get startTimeMs(): number {
|
|
198
|
+
const cachedStartTime = startTimeMsCache.get(this);
|
|
199
|
+
if (cachedStartTime !== undefined) {
|
|
200
|
+
return cachedStartTime;
|
|
201
|
+
}
|
|
175
202
|
const parentTimegroup = this.parentTimegroup;
|
|
176
203
|
if (!parentTimegroup) {
|
|
204
|
+
startTimeMsCache.set(this, 0);
|
|
177
205
|
return 0;
|
|
178
206
|
}
|
|
179
207
|
switch (parentTimegroup.mode) {
|
|
180
208
|
case "sequence": {
|
|
181
209
|
const siblingTemorals = shallowGetTemporalElements(parentTimegroup);
|
|
182
|
-
const ownIndex = siblingTemorals
|
|
210
|
+
const ownIndex = siblingTemorals?.indexOf(
|
|
183
211
|
this as InstanceType<Constructor<TemporalMixinInterface> & T>,
|
|
184
212
|
);
|
|
185
213
|
if (ownIndex === 0) {
|
|
214
|
+
startTimeMsCache.set(this, parentTimegroup.startTimeMs);
|
|
186
215
|
return parentTimegroup.startTimeMs;
|
|
187
216
|
}
|
|
188
|
-
const previous = siblingTemorals[ownIndex - 1];
|
|
217
|
+
const previous = siblingTemorals?.[(ownIndex ?? 0) - 1];
|
|
189
218
|
if (!previous) {
|
|
190
219
|
throw new Error("Previous temporal element not found");
|
|
191
220
|
}
|
|
221
|
+
startTimeMsCache.set(
|
|
222
|
+
this,
|
|
223
|
+
previous.startTimeMs + previous.durationMs,
|
|
224
|
+
);
|
|
192
225
|
return previous.startTimeMs + previous.durationMs;
|
|
193
226
|
}
|
|
194
227
|
case "contain":
|
|
195
228
|
case "fixed":
|
|
229
|
+
startTimeMsCache.set(
|
|
230
|
+
this,
|
|
231
|
+
parentTimegroup.startTimeMs + this.offsetMs,
|
|
232
|
+
);
|
|
196
233
|
return parentTimegroup.startTimeMs + this.offsetMs;
|
|
197
234
|
default:
|
|
198
235
|
throw new Error(`Invalid time mode: ${parentTimegroup.mode}`);
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
html,
|
|
6
6
|
render as litRender,
|
|
7
7
|
} from "lit";
|
|
8
|
-
import { EFTimegroup } from "./EFTimegroup";
|
|
9
|
-
import "./EFTimegroup";
|
|
8
|
+
import { EFTimegroup } from "./EFTimegroup.ts";
|
|
9
|
+
import "./EFTimegroup.ts";
|
|
10
10
|
import { customElement } from "lit/decorators/custom-element.js";
|
|
11
|
-
import { EFTemporal } from "./EFTemporal";
|
|
11
|
+
import { EFTemporal } from "./EFTemporal.ts";
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
for (let i = 0; i < localStorage.length; i++) {
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
isEFTemporal,
|
|
10
10
|
shallowGetTemporalElements,
|
|
11
11
|
timegroupContext,
|
|
12
|
-
} from "./EFTemporal";
|
|
13
|
-
import { TimegroupController } from "./TimegroupController";
|
|
14
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
15
|
-
import { deepGetMediaElements } from "./EFMedia";
|
|
12
|
+
} from "./EFTemporal.ts";
|
|
13
|
+
import { TimegroupController } from "./TimegroupController.ts";
|
|
14
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
15
|
+
import { deepGetMediaElements } from "./EFMedia.ts";
|
|
16
16
|
|
|
17
17
|
const log = debug("ef:elements:EFTimegroup");
|
|
18
18
|
|
|
@@ -113,7 +113,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
113
113
|
connectedCallback() {
|
|
114
114
|
super.connectedCallback();
|
|
115
115
|
if (this.id) {
|
|
116
|
-
this
|
|
116
|
+
this.waitForMediaDurations().then(() => {
|
|
117
|
+
this.#currentTime = this.maybeLoadTimeFromLocalStorage();
|
|
118
|
+
});
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
if (this.parentTimegroup) {
|
|
@@ -265,7 +267,8 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
265
267
|
return (
|
|
266
268
|
EF_INTERACTIVE &&
|
|
267
269
|
this.closest("ef-timegroup") === this &&
|
|
268
|
-
this.closest("ef-workbench") === null
|
|
270
|
+
this.closest("ef-workbench") === null &&
|
|
271
|
+
this.closest("ef-preview") === null
|
|
269
272
|
);
|
|
270
273
|
}
|
|
271
274
|
|
package/src/elements/EFVideo.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { Task } from "@lit/task";
|
|
|
3
3
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
4
4
|
import { customElement } from "lit/decorators.js";
|
|
5
5
|
|
|
6
|
-
import { EFMedia } from "./EFMedia";
|
|
7
|
-
import { TWMixin } from "../gui/TWMixin";
|
|
6
|
+
import { EFMedia } from "./EFMedia.ts";
|
|
7
|
+
import { TWMixin } from "../gui/TWMixin.ts";
|
|
8
8
|
|
|
9
9
|
@customElement("ef-video")
|
|
10
10
|
export class EFVideo extends TWMixin(EFMedia) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { EFAudio } from "./EFAudio";
|
|
1
|
+
import { EFAudio } from "./EFAudio.ts";
|
|
2
2
|
|
|
3
3
|
import { LitElement, html } from "lit";
|
|
4
4
|
import { customElement, property } from "lit/decorators.js";
|
|
5
|
-
import { EFVideo } from "./EFVideo";
|
|
6
|
-
import { EFTemporal } from "./EFTemporal";
|
|
7
|
-
import { CrossUpdateController } from "./CrossUpdateController";
|
|
8
|
-
import { TWMixin } from "../gui/TWMixin";
|
|
5
|
+
import { EFVideo } from "./EFVideo.ts";
|
|
6
|
+
import { EFTemporal } from "./EFTemporal.ts";
|
|
7
|
+
import { CrossUpdateController } from "./CrossUpdateController.ts";
|
|
8
|
+
import { TWMixin } from "../gui/TWMixin.ts";
|
|
9
9
|
import { Task } from "@lit/task";
|
|
10
10
|
import * as d3 from "d3";
|
|
11
11
|
import { type Ref, createRef, ref } from "lit/directives/ref.js";
|
|
12
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
12
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
13
13
|
|
|
14
14
|
@customElement("ef-waveform")
|
|
15
15
|
export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { consume } from "@lit/context";
|
|
2
1
|
import type { LitElement } from "lit";
|
|
3
|
-
import {
|
|
2
|
+
import { consume } from "@lit/context";
|
|
4
3
|
import { state } from "lit/decorators/state.js";
|
|
5
4
|
|
|
5
|
+
import { fetchContext } from "../gui/fetchContext.ts";
|
|
6
|
+
|
|
6
7
|
export declare class FetchMixinInterface {
|
|
7
8
|
fetch: typeof fetch;
|
|
8
9
|
}
|
|
@@ -12,7 +13,8 @@ export function FetchMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
12
13
|
class FetchElement extends superClass {
|
|
13
14
|
@consume({ context: fetchContext, subscribe: true })
|
|
14
15
|
@state()
|
|
15
|
-
fetch
|
|
16
|
+
fetch: (url: string, init?: RequestInit) => Promise<Response> =
|
|
17
|
+
fetch.bind(window);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return FetchElement as Constructor<FetchMixinInterface> & T;
|
package/src/elements/util.ts
CHANGED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { LitElement } from "lit";
|
|
2
|
+
import { provide } from "@lit/context";
|
|
3
|
+
import { property, state } from "lit/decorators.js";
|
|
4
|
+
|
|
5
|
+
import { focusContext, type FocusContext } from "./focusContext.ts";
|
|
6
|
+
import { focusedElementContext } from "./focusedElementContext.ts";
|
|
7
|
+
import { fetchContext } from "./fetchContext.ts";
|
|
8
|
+
import { apiHostContext } from "./apiHostContext.ts";
|
|
9
|
+
import { createRef } from "lit/directives/ref.js";
|
|
10
|
+
import { playingContext } from "./playingContext.ts";
|
|
11
|
+
import type { EFTimegroup } from "../elements/EFTimegroup.ts";
|
|
12
|
+
|
|
13
|
+
declare class ContextMixinInterface {
|
|
14
|
+
focusContext: FocusContext;
|
|
15
|
+
focusedElement?: HTMLElement;
|
|
16
|
+
fetch: typeof fetch;
|
|
17
|
+
signingURL?: string;
|
|
18
|
+
apiToken?: string;
|
|
19
|
+
apiHost: string;
|
|
20
|
+
stageScale: number;
|
|
21
|
+
rendering: boolean;
|
|
22
|
+
stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
|
|
23
|
+
canvasRef: ReturnType<typeof createRef<HTMLElement>>;
|
|
24
|
+
playing: boolean;
|
|
25
|
+
targetTimegroup?: EFTimegroup;
|
|
26
|
+
currentTimeMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
30
|
+
export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
31
|
+
class ContextElement extends superClass {
|
|
32
|
+
@provide({ context: focusContext })
|
|
33
|
+
focusContext = this as FocusContext;
|
|
34
|
+
|
|
35
|
+
@provide({ context: focusedElementContext })
|
|
36
|
+
@state()
|
|
37
|
+
focusedElement?: HTMLElement;
|
|
38
|
+
|
|
39
|
+
@provide({ context: fetchContext })
|
|
40
|
+
fetch = async (url: string, init: RequestInit = {}) => {
|
|
41
|
+
init.headers ||= {};
|
|
42
|
+
Object.assign(init.headers, {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const bearerToken = this.apiToken;
|
|
47
|
+
if (bearerToken) {
|
|
48
|
+
Object.assign(init.headers, {
|
|
49
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.signingURL) {
|
|
54
|
+
if (!this.#signedURLs[url]) {
|
|
55
|
+
this.#signedURLs[url] = fetch(this.signingURL, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify({ url }),
|
|
58
|
+
}).then(async (response) => {
|
|
59
|
+
if (response.ok) {
|
|
60
|
+
return (await response.json()).url;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const signedURL = await this.#signedURLs[url];
|
|
69
|
+
|
|
70
|
+
return fetch(signedURL, init);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fetch(url, init);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
#signedURLs: Record<string, Promise<string>> = {};
|
|
77
|
+
|
|
78
|
+
@property({ type: String })
|
|
79
|
+
signingURL?: string;
|
|
80
|
+
|
|
81
|
+
@property({ type: String })
|
|
82
|
+
apiToken?: string;
|
|
83
|
+
|
|
84
|
+
@provide({ context: apiHostContext })
|
|
85
|
+
@property({ type: String })
|
|
86
|
+
apiHost = "";
|
|
87
|
+
|
|
88
|
+
@provide({ context: playingContext })
|
|
89
|
+
@property({ type: Boolean, reflect: true })
|
|
90
|
+
playing = false;
|
|
91
|
+
|
|
92
|
+
@property({ type: Boolean, reflect: true })
|
|
93
|
+
loop = false;
|
|
94
|
+
|
|
95
|
+
@state()
|
|
96
|
+
stageScale = 1;
|
|
97
|
+
|
|
98
|
+
@property({ type: Boolean })
|
|
99
|
+
rendering = false;
|
|
100
|
+
|
|
101
|
+
@state()
|
|
102
|
+
currentTimeMs = 0;
|
|
103
|
+
|
|
104
|
+
stageRef = createRef<HTMLDivElement>();
|
|
105
|
+
canvasRef = createRef<HTMLSlotElement>();
|
|
106
|
+
|
|
107
|
+
setStageScale = () => {
|
|
108
|
+
if (this.isConnected && !this.rendering) {
|
|
109
|
+
const canvasElement = this.canvasRef.value;
|
|
110
|
+
const stageElement = this.stageRef.value;
|
|
111
|
+
if (stageElement && canvasElement) {
|
|
112
|
+
// Determine the appropriate scale factor to make the canvas fit into
|
|
113
|
+
// it's parent element.
|
|
114
|
+
const stageWidth = stageElement.clientWidth;
|
|
115
|
+
const stageHeight = stageElement.clientHeight;
|
|
116
|
+
const canvasWidth = canvasElement.clientWidth;
|
|
117
|
+
const canvasHeight = canvasElement.clientHeight;
|
|
118
|
+
const stageRatio = stageWidth / stageHeight;
|
|
119
|
+
const canvasRatio = canvasWidth / canvasHeight;
|
|
120
|
+
if (stageRatio > canvasRatio) {
|
|
121
|
+
const scale = stageHeight / canvasHeight;
|
|
122
|
+
if (this.stageScale !== scale) {
|
|
123
|
+
canvasElement.style.transform = `scale(${scale})`;
|
|
124
|
+
}
|
|
125
|
+
this.stageScale = scale;
|
|
126
|
+
} else {
|
|
127
|
+
const scale = stageWidth / canvasWidth;
|
|
128
|
+
if (this.stageScale !== scale) {
|
|
129
|
+
canvasElement.style.transform = `scale(${scale})`;
|
|
130
|
+
}
|
|
131
|
+
this.stageScale = scale;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (this.isConnected) {
|
|
136
|
+
requestAnimationFrame(this.setStageScale);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
connectedCallback(): void {
|
|
141
|
+
super.connectedCallback();
|
|
142
|
+
// Preferrably we would use a resizeObserver, but it is difficult to get the first resize
|
|
143
|
+
// timed correctly. So we use requestAnimationFrame as a stop-gap.
|
|
144
|
+
requestAnimationFrame(this.setStageScale);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
update(changedProperties: Map<string | number | symbol, unknown>) {
|
|
148
|
+
if (changedProperties.has("playing")) {
|
|
149
|
+
if (this.playing) {
|
|
150
|
+
this.#startPlayback();
|
|
151
|
+
} else {
|
|
152
|
+
this.#stopPlayback();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
|
|
157
|
+
if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
|
|
158
|
+
this.targetTimegroup.currentTimeMs = this.currentTimeMs;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
super.update(changedProperties);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get targetTimegroup() {
|
|
165
|
+
return this.querySelector("ef-timegroup");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#playbackAudioContext: AudioContext | null = null;
|
|
169
|
+
#playbackAnimationFrameRequest: number | null = null;
|
|
170
|
+
#AUDIO_PLAYBACK_SLICE_MS = 1000;
|
|
171
|
+
|
|
172
|
+
#syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
|
|
173
|
+
this.currentTimeMs =
|
|
174
|
+
startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
|
|
175
|
+
this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
|
|
176
|
+
this.#syncPlayheadToAudioContext(target, startMs);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async #stopPlayback() {
|
|
181
|
+
if (this.#playbackAudioContext) {
|
|
182
|
+
if (this.#playbackAudioContext.state !== "closed") {
|
|
183
|
+
await this.#playbackAudioContext.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (this.#playbackAnimationFrameRequest) {
|
|
187
|
+
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
188
|
+
}
|
|
189
|
+
this.#playbackAudioContext = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async #startPlayback() {
|
|
193
|
+
await this.#stopPlayback();
|
|
194
|
+
const timegroup = this.targetTimegroup;
|
|
195
|
+
if (!timegroup) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let currentMs = timegroup.currentTimeMs;
|
|
200
|
+
let bufferCount = 0;
|
|
201
|
+
this.#playbackAudioContext = new AudioContext({
|
|
202
|
+
latencyHint: "playback",
|
|
203
|
+
});
|
|
204
|
+
if (this.#playbackAnimationFrameRequest) {
|
|
205
|
+
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
206
|
+
}
|
|
207
|
+
this.#syncPlayheadToAudioContext(timegroup, currentMs);
|
|
208
|
+
const playbackContext = this.#playbackAudioContext;
|
|
209
|
+
await playbackContext.suspend();
|
|
210
|
+
|
|
211
|
+
const fillBuffer = async () => {
|
|
212
|
+
if (bufferCount > 1) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const canFillBuffer = await queueBufferSource();
|
|
216
|
+
if (canFillBuffer) {
|
|
217
|
+
fillBuffer();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const fromMs = currentMs;
|
|
222
|
+
const toMs = timegroup.endTimeMs;
|
|
223
|
+
|
|
224
|
+
const queueBufferSource = async () => {
|
|
225
|
+
if (currentMs >= toMs) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const startMs = currentMs;
|
|
229
|
+
const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
230
|
+
currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
231
|
+
const audioBuffer = await timegroup.renderAudio(startMs, endMs);
|
|
232
|
+
bufferCount++;
|
|
233
|
+
const source = playbackContext.createBufferSource();
|
|
234
|
+
source.buffer = audioBuffer;
|
|
235
|
+
source.connect(playbackContext.destination);
|
|
236
|
+
source.start((startMs - fromMs) / 1000);
|
|
237
|
+
source.onended = () => {
|
|
238
|
+
bufferCount--;
|
|
239
|
+
if (endMs >= toMs) {
|
|
240
|
+
this.playing = false;
|
|
241
|
+
} else {
|
|
242
|
+
fillBuffer();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
return true;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await fillBuffer();
|
|
249
|
+
await playbackContext.resume();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return ContextElement as Constructor<ContextMixinInterface> & T;
|
|
254
|
+
}
|