@editframe/elements 0.14.0-beta.3 → 0.15.0-beta.3
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.js +0 -2
- package/dist/elements/EFAudio.d.ts +0 -1
- package/dist/elements/EFAudio.js +1 -5
- package/dist/elements/EFCaptions.js +1 -1
- package/dist/elements/EFImage.d.ts +2 -1
- package/dist/elements/EFImage.js +9 -3
- package/dist/elements/EFMedia.d.ts +7 -0
- package/dist/elements/EFMedia.js +159 -8
- package/dist/elements/EFTemporal.d.ts +7 -3
- package/dist/elements/EFTemporal.js +19 -1
- package/dist/elements/EFTimegroup.d.ts +1 -5
- package/dist/elements/EFTimegroup.js +5 -6
- package/dist/elements/EFWaveform.d.ts +16 -8
- package/dist/elements/EFWaveform.js +244 -164
- package/dist/elements/TargetController.d.ts +25 -0
- package/dist/elements/TargetController.js +164 -0
- package/dist/elements/TargetController.test.d.ts +19 -0
- package/dist/gui/EFPreview.d.ts +1 -1
- package/dist/gui/EFPreview.js +1 -0
- package/dist/gui/EFWorkbench.js +1 -1
- package/package.json +3 -2
- package/src/elements/EFAudio.ts +1 -4
- package/src/elements/EFCaptions.ts +1 -1
- package/src/elements/EFImage.browsertest.ts +33 -2
- package/src/elements/EFImage.ts +10 -3
- package/src/elements/EFMedia.ts +187 -6
- package/src/elements/EFTemporal.ts +37 -5
- package/src/elements/EFTimegroup.ts +5 -7
- package/src/elements/EFWaveform.ts +302 -204
- package/src/elements/TargetController.test.ts +229 -0
- package/src/elements/TargetController.ts +219 -0
- package/src/gui/EFPreview.ts +10 -9
- package/src/gui/EFWorkbench.ts +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
const EF_TARGETABLE = Symbol("EF_TARGETABLE");
|
|
3
|
+
class TargetRegistry {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.idMap = /* @__PURE__ */ new Map();
|
|
6
|
+
this.callbacks = /* @__PURE__ */ new Map();
|
|
7
|
+
}
|
|
8
|
+
subscribe(id, callback) {
|
|
9
|
+
this.callbacks.set(id, this.callbacks.get(id) ?? /* @__PURE__ */ new Set());
|
|
10
|
+
this.callbacks.get(id)?.add(callback);
|
|
11
|
+
}
|
|
12
|
+
unsubscribe(id, callback) {
|
|
13
|
+
if (id === null) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.callbacks.get(id)?.delete(callback);
|
|
17
|
+
if (this.callbacks.get(id)?.size === 0) {
|
|
18
|
+
this.callbacks.delete(id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
get(id) {
|
|
22
|
+
return this.idMap.get(id);
|
|
23
|
+
}
|
|
24
|
+
register(id, target) {
|
|
25
|
+
this.idMap.set(id, target);
|
|
26
|
+
for (const callback of this.callbacks.get(id) ?? []) {
|
|
27
|
+
callback(target);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
unregister(id) {
|
|
31
|
+
for (const callback of this.callbacks.get(id) ?? []) {
|
|
32
|
+
callback(void 0);
|
|
33
|
+
}
|
|
34
|
+
this.idMap.delete(id);
|
|
35
|
+
this.callbacks.delete(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const documentRegistries = /* @__PURE__ */ new WeakMap();
|
|
39
|
+
const getRegistry = (root) => {
|
|
40
|
+
let registry = documentRegistries.get(root);
|
|
41
|
+
if (!registry) {
|
|
42
|
+
registry = new TargetRegistry();
|
|
43
|
+
documentRegistries.set(root, registry);
|
|
44
|
+
}
|
|
45
|
+
return registry;
|
|
46
|
+
};
|
|
47
|
+
const EFTargetable = (superClass) => {
|
|
48
|
+
class TargetableElement extends superClass {
|
|
49
|
+
#registry = null;
|
|
50
|
+
static get observedAttributes() {
|
|
51
|
+
const parentAttributes = superClass.observedAttributes || [];
|
|
52
|
+
return [.../* @__PURE__ */ new Set([...parentAttributes, "id"])];
|
|
53
|
+
}
|
|
54
|
+
updateRegistry(oldValue, newValue) {
|
|
55
|
+
if (!this.#registry) return;
|
|
56
|
+
if (oldValue === newValue) return;
|
|
57
|
+
if (oldValue) {
|
|
58
|
+
this.#registry.unregister(oldValue);
|
|
59
|
+
}
|
|
60
|
+
if (newValue) {
|
|
61
|
+
this.#registry.register(newValue, this);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
super.connectedCallback();
|
|
66
|
+
this.#registry = getRegistry(this.getRootNode());
|
|
67
|
+
const initialId = this.getAttribute("id");
|
|
68
|
+
if (initialId) {
|
|
69
|
+
this.updateRegistry("", initialId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
attributeChangedCallback(name, old, value) {
|
|
73
|
+
super.attributeChangedCallback(name, old, value);
|
|
74
|
+
if (name === "id") {
|
|
75
|
+
this.updateRegistry(old ?? "", value ?? "");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
disconnectedCallback() {
|
|
79
|
+
if (this.#registry) {
|
|
80
|
+
this.updateRegistry(this.id, "");
|
|
81
|
+
this.#registry = null;
|
|
82
|
+
}
|
|
83
|
+
super.disconnectedCallback();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
Object.defineProperty(TargetableElement.prototype, EF_TARGETABLE, {
|
|
87
|
+
value: true
|
|
88
|
+
});
|
|
89
|
+
return TargetableElement;
|
|
90
|
+
};
|
|
91
|
+
class TargetUpdateController {
|
|
92
|
+
constructor(host) {
|
|
93
|
+
this.host = host;
|
|
94
|
+
}
|
|
95
|
+
hostConnected() {
|
|
96
|
+
this.host.requestUpdate();
|
|
97
|
+
}
|
|
98
|
+
hostDisconnected() {
|
|
99
|
+
this.host.requestUpdate();
|
|
100
|
+
}
|
|
101
|
+
hostUpdate() {
|
|
102
|
+
this.host.requestUpdate();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
class TargetController {
|
|
106
|
+
constructor(host) {
|
|
107
|
+
this.targetController = null;
|
|
108
|
+
this.currentTargetString = null;
|
|
109
|
+
this.registryCallback = (target) => {
|
|
110
|
+
this.host.targetElement = target ?? null;
|
|
111
|
+
};
|
|
112
|
+
this.host = host;
|
|
113
|
+
this.host.addController(this);
|
|
114
|
+
this.currentTargetString = this.host.target;
|
|
115
|
+
if (this.currentTargetString) {
|
|
116
|
+
this.registry.subscribe(this.currentTargetString, this.registryCallback);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
updateTarget() {
|
|
120
|
+
const newTarget = this.registry.get(this.host.target);
|
|
121
|
+
if (this.host.targetElement !== newTarget) {
|
|
122
|
+
this.disconnectFromTarget();
|
|
123
|
+
this.host.targetElement = newTarget ?? null;
|
|
124
|
+
this.connectToTarget();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
connectToTarget() {
|
|
128
|
+
if (this.host.targetElement instanceof LitElement) {
|
|
129
|
+
this.targetController = new TargetUpdateController(this.host);
|
|
130
|
+
this.host.targetElement.addController(this.targetController);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
disconnectFromTarget() {
|
|
134
|
+
if (this.host.targetElement instanceof LitElement && this.targetController) {
|
|
135
|
+
this.host.targetElement.removeController(this.targetController);
|
|
136
|
+
this.targetController = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
get registry() {
|
|
140
|
+
const root = this.host.getRootNode();
|
|
141
|
+
return getRegistry(root);
|
|
142
|
+
}
|
|
143
|
+
hostDisconnected() {
|
|
144
|
+
this.disconnectFromTarget();
|
|
145
|
+
}
|
|
146
|
+
hostConnected() {
|
|
147
|
+
this.updateTarget();
|
|
148
|
+
}
|
|
149
|
+
hostUpdate() {
|
|
150
|
+
if (this.currentTargetString !== this.host.target) {
|
|
151
|
+
this.registry.unsubscribe(
|
|
152
|
+
this.currentTargetString,
|
|
153
|
+
this.registryCallback
|
|
154
|
+
);
|
|
155
|
+
this.registry.subscribe(this.host.target, this.registryCallback);
|
|
156
|
+
this.updateTarget();
|
|
157
|
+
this.currentTargetString = this.host.target;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
EFTargetable,
|
|
163
|
+
TargetController
|
|
164
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
declare const TargetableTest_base: typeof LitElement;
|
|
3
|
+
declare class TargetableTest extends TargetableTest_base {
|
|
4
|
+
value: string;
|
|
5
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
6
|
+
}
|
|
7
|
+
declare class TargeterTest extends LitElement {
|
|
8
|
+
private targetController;
|
|
9
|
+
targetElement: Element | null;
|
|
10
|
+
target: string;
|
|
11
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
12
|
+
}
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap {
|
|
15
|
+
"targetable-test": TargetableTest & Element;
|
|
16
|
+
"targeter-test": TargeterTest & Element;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {};
|
package/dist/gui/EFPreview.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
2
|
declare const EFPreview_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
|
|
3
3
|
export declare class EFPreview extends EFPreview_base {
|
|
4
|
+
static styles: import('lit').CSSResult[];
|
|
4
5
|
focusedElement?: HTMLElement;
|
|
5
6
|
constructor();
|
|
6
|
-
static styles: import('lit').CSSResult[];
|
|
7
7
|
render(): import('lit-html').TemplateResult<1>;
|
|
8
8
|
}
|
|
9
9
|
declare global {
|
package/dist/gui/EFPreview.js
CHANGED
package/dist/gui/EFWorkbench.js
CHANGED
|
@@ -62,7 +62,7 @@ let EFWorkbench = class extends ContextMixin(TWMixin(LitElement)) {
|
|
|
62
62
|
focusOverlay.style.display = "block";
|
|
63
63
|
const rect = this.focusedElement.getBoundingClientRect();
|
|
64
64
|
Object.assign(focusOverlay.style, {
|
|
65
|
-
position: "
|
|
65
|
+
position: "fixed",
|
|
66
66
|
top: `${rect.top}px`,
|
|
67
67
|
left: `${rect.left}px`,
|
|
68
68
|
width: `${rect.width}px`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0-beta.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"author": "",
|
|
22
22
|
"license": "UNLICENSED",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@
|
|
24
|
+
"@bramus/style-observer": "^1.3.0",
|
|
25
|
+
"@editframe/assets": "0.15.0-beta.3",
|
|
25
26
|
"@lit/context": "^1.1.2",
|
|
26
27
|
"@lit/task": "^1.0.1",
|
|
27
28
|
"d3": "^7.9.0",
|
package/src/elements/EFAudio.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
2
|
import { html } from "lit";
|
|
3
|
-
import { customElement
|
|
3
|
+
import { customElement } from "lit/decorators.js";
|
|
4
4
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
5
|
import { EFMedia } from "./EFMedia.js";
|
|
6
6
|
|
|
@@ -8,9 +8,6 @@ import { EFMedia } from "./EFMedia.js";
|
|
|
8
8
|
export class EFAudio extends EFMedia {
|
|
9
9
|
audioElementRef = createRef<HTMLAudioElement>();
|
|
10
10
|
|
|
11
|
-
@property({ type: String })
|
|
12
|
-
src = "";
|
|
13
|
-
|
|
14
11
|
render() {
|
|
15
12
|
return html`<audio ${ref(this.audioElementRef)}></audio>`;
|
|
16
13
|
}
|
|
@@ -373,7 +373,7 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
373
373
|
return;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
const currentTimeMs = this.targetElement.
|
|
376
|
+
const currentTimeMs = this.targetElement.currentSourceTimeMs;
|
|
377
377
|
const currentTimeSec = currentTimeMs / 1000;
|
|
378
378
|
|
|
379
379
|
// Find the current word from word_segments
|
|
@@ -18,9 +18,9 @@ describe("EFImage", () => {
|
|
|
18
18
|
const workbench = document.createElement("ef-workbench");
|
|
19
19
|
const element = document.createElement("ef-image");
|
|
20
20
|
workbench.appendChild(element);
|
|
21
|
-
element.assetId = "550e8400-e29b-41d4-a716-446655440000
|
|
21
|
+
element.assetId = "550e8400-e29b-41d4-a716-446655440000";
|
|
22
22
|
expect(element.assetPath()).toBe(
|
|
23
|
-
"editframe://api/v1/image_files/550e8400-e29b-41d4-a716-446655440000
|
|
23
|
+
"editframe://api/v1/image_files/550e8400-e29b-41d4-a716-446655440000",
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
});
|
|
@@ -46,4 +46,35 @@ describe("EFImage", () => {
|
|
|
46
46
|
expect(image.assetPath()).toBe(`test:///api/v1/image_files/${id}`);
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
describe("hasOwnDuration", () => {
|
|
51
|
+
test("is false by default", () => {
|
|
52
|
+
const image = document.createElement("ef-image");
|
|
53
|
+
expect(image.hasOwnDuration).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("is true when duration is set", () => {
|
|
57
|
+
const image = document.createElement("ef-image");
|
|
58
|
+
image.setAttribute("duration", "1s");
|
|
59
|
+
expect(image.hasOwnDuration).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("durationMs", () => {
|
|
64
|
+
test("Can be set on element directly", () => {
|
|
65
|
+
const image = document.createElement("ef-image");
|
|
66
|
+
image.src =
|
|
67
|
+
"https://editframe.dev/api/v1/image_files/550e8400-e29b-41d4-a716-446655440000";
|
|
68
|
+
image.duration = "1s";
|
|
69
|
+
expect(image.durationMs).toBe(1000);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("Can be set through setAttribute", () => {
|
|
73
|
+
const image = document.createElement("ef-image");
|
|
74
|
+
image.src =
|
|
75
|
+
"https://editframe.dev/api/v1/image_files/550e8400-e29b-41d4-a716-446655440000";
|
|
76
|
+
image.setAttribute("duration", "1s");
|
|
77
|
+
expect(image.durationMs).toBe(1000);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
49
80
|
});
|
package/src/elements/EFImage.ts
CHANGED
|
@@ -5,12 +5,15 @@ import { createRef, ref } from "lit/directives/ref.js";
|
|
|
5
5
|
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
6
6
|
import { EF_RENDERING } from "../EF_RENDERING.js";
|
|
7
7
|
import { EFSourceMixin } from "./EFSourceMixin.js";
|
|
8
|
+
import { EFTemporal } from "./EFTemporal.js";
|
|
8
9
|
import { FetchMixin } from "./FetchMixin.js";
|
|
9
10
|
|
|
10
11
|
@customElement("ef-image")
|
|
11
|
-
export class EFImage extends
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export class EFImage extends EFTemporal(
|
|
13
|
+
EFSourceMixin(FetchMixin(LitElement), {
|
|
14
|
+
assetType: "image_files",
|
|
15
|
+
}),
|
|
16
|
+
) {
|
|
14
17
|
static styles = [
|
|
15
18
|
css`
|
|
16
19
|
:host {
|
|
@@ -52,6 +55,10 @@ export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
|
|
|
52
55
|
return `/@ef-image/${this.src}`;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
get hasOwnDuration() {
|
|
59
|
+
return this.hasExplicitDuration;
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
fetchImage = new Task(this, {
|
|
56
63
|
autoRun: EF_INTERACTIVE,
|
|
57
64
|
args: () => [this.assetPath(), this.fetch] as const,
|
package/src/elements/EFMedia.ts
CHANGED
|
@@ -14,9 +14,44 @@ import { EF_RENDERING } from "../EF_RENDERING.js";
|
|
|
14
14
|
import { EFSourceMixin } from "./EFSourceMixin.js";
|
|
15
15
|
import { EFTemporal, isEFTemporal } from "./EFTemporal.js";
|
|
16
16
|
import { FetchMixin } from "./FetchMixin.js";
|
|
17
|
+
import { EFTargetable } from "./TargetController.ts";
|
|
17
18
|
|
|
18
19
|
const log = debug("ef:elements:EFMedia");
|
|
19
20
|
|
|
21
|
+
const freqWeightsCache = new Map<number, Float32Array>();
|
|
22
|
+
|
|
23
|
+
class LRUCache<K, V> {
|
|
24
|
+
private cache = new Map<K, V>();
|
|
25
|
+
private readonly maxSize: number;
|
|
26
|
+
|
|
27
|
+
constructor(maxSize: number) {
|
|
28
|
+
this.maxSize = maxSize;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(key: K): V | undefined {
|
|
32
|
+
const value = this.cache.get(key);
|
|
33
|
+
if (value) {
|
|
34
|
+
// Refresh position by removing and re-adding
|
|
35
|
+
this.cache.delete(key);
|
|
36
|
+
this.cache.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(key: K, value: V): void {
|
|
42
|
+
if (this.cache.has(key)) {
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
45
|
+
// Remove oldest entry (first item in map)
|
|
46
|
+
const firstKey = this.cache.keys().next().value;
|
|
47
|
+
if (firstKey) {
|
|
48
|
+
this.cache.delete(firstKey);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.cache.set(key, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
export const deepGetMediaElements = (
|
|
21
56
|
element: Element,
|
|
22
57
|
medias: EFMedia[] = [],
|
|
@@ -31,9 +66,11 @@ export const deepGetMediaElements = (
|
|
|
31
66
|
return medias;
|
|
32
67
|
};
|
|
33
68
|
|
|
34
|
-
export class EFMedia extends
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
export class EFMedia extends EFTargetable(
|
|
70
|
+
EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
71
|
+
assetType: "isobmff_files",
|
|
72
|
+
}),
|
|
73
|
+
) {
|
|
37
74
|
static styles = [
|
|
38
75
|
css`
|
|
39
76
|
:host {
|
|
@@ -284,7 +321,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
284
321
|
},
|
|
285
322
|
});
|
|
286
323
|
|
|
287
|
-
@state()
|
|
324
|
+
@state()
|
|
325
|
+
desiredSeekTimeMs = 0;
|
|
288
326
|
|
|
289
327
|
protected async executeSeek(seekToMs: number) {
|
|
290
328
|
this.desiredSeekTimeMs = seekToMs;
|
|
@@ -294,7 +332,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
294
332
|
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
295
333
|
): void {
|
|
296
334
|
if (changedProperties.has("ownCurrentTimeMs")) {
|
|
297
|
-
this.executeSeek(this.
|
|
335
|
+
this.executeSeek(this.currentSourceTimeMs);
|
|
298
336
|
}
|
|
299
337
|
// TODO: this is copied straight from EFTimegroup.ts
|
|
300
338
|
// and should be refactored to be shared/reduce bad duplication of
|
|
@@ -312,7 +350,6 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
312
350
|
return;
|
|
313
351
|
}
|
|
314
352
|
this.style.display = "";
|
|
315
|
-
|
|
316
353
|
const animations = this.getAnimations({ subtree: true });
|
|
317
354
|
|
|
318
355
|
this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
|
|
@@ -546,4 +583,148 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
|
|
|
546
583
|
this.trimEndMs,
|
|
547
584
|
};
|
|
548
585
|
}
|
|
586
|
+
|
|
587
|
+
@property({ type: Number })
|
|
588
|
+
fftSize = 512; // Default FFT size
|
|
589
|
+
|
|
590
|
+
@property({ type: Number })
|
|
591
|
+
fftDecay = 8; // Default number of frames to analyze
|
|
592
|
+
|
|
593
|
+
private static readonly MIN_DB = -90;
|
|
594
|
+
private static readonly MAX_DB = -20;
|
|
595
|
+
private static readonly DECAY_WEIGHT = 0.7;
|
|
596
|
+
|
|
597
|
+
// Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
|
|
598
|
+
get FREQ_WEIGHTS() {
|
|
599
|
+
if (freqWeightsCache.has(this.fftSize)) {
|
|
600
|
+
// biome-ignore lint/style/noNonNullAssertion: Will exist due to prior has check
|
|
601
|
+
return freqWeightsCache.get(this.fftSize)!;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
|
|
605
|
+
const frequency = (i * 48000) / this.fftSize;
|
|
606
|
+
if (frequency < 60) return 0.3;
|
|
607
|
+
if (frequency < 250) return 0.4;
|
|
608
|
+
if (frequency < 500) return 0.6;
|
|
609
|
+
if (frequency < 2000) return 0.8;
|
|
610
|
+
if (frequency < 4000) return 1.2;
|
|
611
|
+
if (frequency < 8000) return 1.6;
|
|
612
|
+
return 2.0;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
freqWeightsCache.set(this.fftSize, weights);
|
|
616
|
+
return weights;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#frequencyDataCache = new LRUCache<string, Uint8Array>(100);
|
|
620
|
+
|
|
621
|
+
frequencyDataTask = new Task(this, {
|
|
622
|
+
autoRun: EF_INTERACTIVE,
|
|
623
|
+
args: () =>
|
|
624
|
+
[
|
|
625
|
+
this.audioBufferTask.status,
|
|
626
|
+
this.currentSourceTimeMs,
|
|
627
|
+
this.fftSize, // Add fftSize to dependency array
|
|
628
|
+
this.fftDecay, // Add fftDecay to dependency array
|
|
629
|
+
] as const,
|
|
630
|
+
task: async () => {
|
|
631
|
+
await this.audioBufferTask.taskComplete;
|
|
632
|
+
if (!this.audioBufferTask.value) return null;
|
|
633
|
+
if (this.currentSourceTimeMs <= 0) return null;
|
|
634
|
+
|
|
635
|
+
const currentTimeMs = this.currentSourceTimeMs;
|
|
636
|
+
const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
|
|
637
|
+
const audioBuffer = this.audioBufferTask.value.buffer;
|
|
638
|
+
const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
|
|
639
|
+
|
|
640
|
+
const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
|
|
641
|
+
if (cachedSmoothedData) {
|
|
642
|
+
return cachedSmoothedData;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const framesData = await Promise.all(
|
|
646
|
+
Array.from({ length: this.fftDecay }, async (_, i) => {
|
|
647
|
+
const frameOffset = i * (1000 / 30);
|
|
648
|
+
const startTime = Math.max(
|
|
649
|
+
0,
|
|
650
|
+
(currentTimeMs - frameOffset - startOffsetMs) / 1000,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// Cache key for this specific frame
|
|
654
|
+
const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
|
|
655
|
+
|
|
656
|
+
// Check cache for this specific frame
|
|
657
|
+
const cachedFrame = this.#frequencyDataCache.get(cacheKey);
|
|
658
|
+
if (cachedFrame) {
|
|
659
|
+
return cachedFrame;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const audioContext = new OfflineAudioContext(
|
|
663
|
+
2,
|
|
664
|
+
48000 * (1 / 30),
|
|
665
|
+
48000,
|
|
666
|
+
);
|
|
667
|
+
const analyser = audioContext.createAnalyser();
|
|
668
|
+
analyser.fftSize = this.fftSize;
|
|
669
|
+
analyser.minDecibels = EFMedia.MIN_DB;
|
|
670
|
+
analyser.maxDecibels = EFMedia.MAX_DB;
|
|
671
|
+
|
|
672
|
+
const audioBufferSource = audioContext.createBufferSource();
|
|
673
|
+
audioBufferSource.buffer = audioBuffer;
|
|
674
|
+
|
|
675
|
+
audioBufferSource.connect(analyser);
|
|
676
|
+
analyser.connect(audioContext.destination);
|
|
677
|
+
|
|
678
|
+
audioBufferSource.start(0, startTime, 1 / 30);
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
await audioContext.startRendering();
|
|
682
|
+
const frameData = new Uint8Array(this.fftSize / 2);
|
|
683
|
+
analyser.getByteFrequencyData(frameData);
|
|
684
|
+
|
|
685
|
+
// Cache this frame's analysis
|
|
686
|
+
this.#frequencyDataCache.set(cacheKey, frameData);
|
|
687
|
+
return frameData;
|
|
688
|
+
} finally {
|
|
689
|
+
audioBufferSource.disconnect();
|
|
690
|
+
analyser.disconnect();
|
|
691
|
+
}
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const frameLength = framesData[0]?.length ?? 0;
|
|
696
|
+
|
|
697
|
+
// Combine frames with decay
|
|
698
|
+
const smoothedData = new Uint8Array(frameLength);
|
|
699
|
+
for (let i = 0; i < frameLength; i++) {
|
|
700
|
+
let weightedSum = 0;
|
|
701
|
+
let weightSum = 0;
|
|
702
|
+
|
|
703
|
+
framesData.forEach((frame, frameIndex) => {
|
|
704
|
+
const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
|
|
705
|
+
// biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
|
|
706
|
+
weightedSum += frame[i]! * decayWeight;
|
|
707
|
+
weightSum += decayWeight;
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Apply frequency weights using instance FREQ_WEIGHTS
|
|
714
|
+
smoothedData.forEach((value, i) => {
|
|
715
|
+
// biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
|
|
716
|
+
const freqWeight = this.FREQ_WEIGHTS[i]!;
|
|
717
|
+
smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Only return the lower half of the frequency data
|
|
721
|
+
// The top half is zeroed out, which makes for aesthetically unpleasing waveforms
|
|
722
|
+
const slicedData = smoothedData.slice(
|
|
723
|
+
0,
|
|
724
|
+
Math.floor(smoothedData.length / 2),
|
|
725
|
+
);
|
|
726
|
+
this.#frequencyDataCache.set(smoothedKey, slicedData);
|
|
727
|
+
return slicedData;
|
|
728
|
+
},
|
|
729
|
+
});
|
|
549
730
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { consume, createContext } from "@lit/context";
|
|
2
|
-
import type { LitElement, ReactiveController } from "lit";
|
|
2
|
+
import type { LitElement, PropertyValueMap, ReactiveController } from "lit";
|
|
3
3
|
import { property, state } from "lit/decorators.js";
|
|
4
4
|
import type { EFTimegroup } from "./EFTimegroup.js";
|
|
5
5
|
|
|
@@ -13,6 +13,10 @@ export const timegroupContext = createContext<EFTimegroup>(
|
|
|
13
13
|
|
|
14
14
|
export declare class TemporalMixinInterface {
|
|
15
15
|
get hasOwnDuration(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the element has a duration set as an attribute.
|
|
18
|
+
*/
|
|
19
|
+
get hasExplicitDuration(): boolean;
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Used to trim the start of the media.
|
|
@@ -147,18 +151,18 @@ export declare class TemporalMixinInterface {
|
|
|
147
151
|
* elements.
|
|
148
152
|
*
|
|
149
153
|
* For example, if the media has a `sourcein` value of 10s, when `ownCurrentTimeMs` is 0s,
|
|
150
|
-
* `
|
|
154
|
+
* `currentSourceTimeMs` will be 10s.
|
|
151
155
|
*
|
|
152
156
|
* sourcein=10s sourceout=10s
|
|
153
157
|
* / / /
|
|
154
158
|
* |--------|=================|---------|
|
|
155
159
|
* ^
|
|
156
160
|
* |_
|
|
157
|
-
*
|
|
161
|
+
* currentSourceTimeMs === 10s
|
|
158
162
|
* |_
|
|
159
163
|
* ownCurrentTimeMs === 0s
|
|
160
164
|
*/
|
|
161
|
-
get
|
|
165
|
+
get currentSourceTimeMs(): number;
|
|
162
166
|
|
|
163
167
|
set duration(value: string);
|
|
164
168
|
get duration(): string;
|
|
@@ -461,6 +465,10 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
|
461
465
|
return parent as EFTimegroup | undefined;
|
|
462
466
|
}
|
|
463
467
|
|
|
468
|
+
get hasExplicitDuration() {
|
|
469
|
+
return this._durationMs !== undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
464
472
|
get hasOwnDuration() {
|
|
465
473
|
return false;
|
|
466
474
|
}
|
|
@@ -559,6 +567,10 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
|
559
567
|
return this.startTimeMs + this.durationMs;
|
|
560
568
|
}
|
|
561
569
|
|
|
570
|
+
/**
|
|
571
|
+
* The current time of the element within itself.
|
|
572
|
+
* Compare with `currentTimeMs` to see the current time with respect to the root timegroup
|
|
573
|
+
*/
|
|
562
574
|
get ownCurrentTimeMs() {
|
|
563
575
|
if (this.rootTimegroup) {
|
|
564
576
|
return Math.min(
|
|
@@ -573,7 +585,7 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
|
573
585
|
* Used to calculate the internal currentTimeMs of the element. This is useful
|
|
574
586
|
* for mapping to internal media time codes for audio/video elements.
|
|
575
587
|
*/
|
|
576
|
-
get
|
|
588
|
+
get currentSourceTimeMs() {
|
|
577
589
|
if (this.rootTimegroup) {
|
|
578
590
|
if (this.sourceInMs && this.sourceOutMs) {
|
|
579
591
|
return Math.min(
|
|
@@ -613,6 +625,26 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
|
|
|
613
625
|
}
|
|
614
626
|
},
|
|
615
627
|
});
|
|
628
|
+
|
|
629
|
+
protected updated(
|
|
630
|
+
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
631
|
+
): void {
|
|
632
|
+
super.updated(changedProperties);
|
|
633
|
+
if (
|
|
634
|
+
changedProperties.has("currentTime") ||
|
|
635
|
+
changedProperties.has("ownCurrentTimeMs")
|
|
636
|
+
) {
|
|
637
|
+
const timelineTimeMs = (this.rootTimegroup ?? this).ownCurrentTimeMs;
|
|
638
|
+
if (
|
|
639
|
+
this.startTimeMs > timelineTimeMs ||
|
|
640
|
+
this.endTimeMs < timelineTimeMs
|
|
641
|
+
) {
|
|
642
|
+
this.style.display = "none";
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
this.style.display = "";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
616
648
|
}
|
|
617
649
|
|
|
618
650
|
Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, {
|