@editframe/elements 0.6.0-beta.15 → 0.6.0-beta.17
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/package.json +2 -2
- package/src/elements/CrossUpdateController.ts +22 -0
- package/src/elements/EFAudio.ts +40 -0
- package/src/elements/EFCaptions.ts +188 -0
- package/src/elements/EFImage.ts +68 -0
- package/src/elements/EFMedia.ts +384 -0
- package/src/elements/EFSourceMixin.ts +57 -0
- package/src/elements/EFTemporal.ts +231 -0
- package/src/elements/EFTimegroup.browsertest.ts +333 -0
- package/src/elements/EFTimegroup.ts +381 -0
- package/src/elements/EFTimeline.ts +13 -0
- package/src/elements/EFVideo.ts +103 -0
- package/src/elements/EFWaveform.ts +409 -0
- package/src/elements/FetchMixin.ts +19 -0
- package/src/elements/TimegroupController.ts +25 -0
- package/src/elements/durationConverter.ts +6 -0
- package/src/elements/parseTimeToMs.ts +9 -0
- package/src/elements/util.ts +24 -0
- package/src/gui/EFFilmstrip.ts +766 -0
- package/src/gui/EFWorkbench.ts +231 -0
- package/src/gui/TWMixin.css +3 -0
- package/src/gui/TWMixin.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.17",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "",
|
|
20
20
|
"license": "UNLICENSED",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@editframe/assets": "0.6.0-beta.
|
|
22
|
+
"@editframe/assets": "0.6.0-beta.17",
|
|
23
23
|
"@lit/context": "^1.1.1",
|
|
24
24
|
"@lit/task": "^1.0.0",
|
|
25
25
|
"d3": "^7.9.0",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LitElement,
|
|
3
|
+
ReactiveController,
|
|
4
|
+
ReactiveControllerHost,
|
|
5
|
+
} from "lit";
|
|
6
|
+
|
|
7
|
+
export class CrossUpdateController implements ReactiveController {
|
|
8
|
+
constructor(
|
|
9
|
+
private host: ReactiveControllerHost,
|
|
10
|
+
private target: LitElement,
|
|
11
|
+
) {
|
|
12
|
+
this.host.addController(this);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
hostUpdate(): void {
|
|
16
|
+
this.target.requestUpdate();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
remove(): void {
|
|
20
|
+
this.host.removeController(this);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { html } from "lit";
|
|
2
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import { EFMedia } from "./EFMedia";
|
|
5
|
+
import { Task } from "@lit/task";
|
|
6
|
+
|
|
7
|
+
@customElement("ef-audio")
|
|
8
|
+
export class EFAudio extends EFMedia {
|
|
9
|
+
audioElementRef = createRef<HTMLAudioElement>();
|
|
10
|
+
|
|
11
|
+
@property({ type: String })
|
|
12
|
+
src = "";
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
return html`<audio ${ref(this.audioElementRef)}></audio>`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get audioElement() {
|
|
19
|
+
return this.audioElementRef.value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
frameTask = new Task(this, {
|
|
23
|
+
args: () =>
|
|
24
|
+
[
|
|
25
|
+
this.trackFragmentIndexLoader.status,
|
|
26
|
+
this.initSegmentsLoader.status,
|
|
27
|
+
this.seekTask.status,
|
|
28
|
+
this.fetchSeekTask.status,
|
|
29
|
+
this.videoAssetTask.status,
|
|
30
|
+
] as const,
|
|
31
|
+
task: async () => {
|
|
32
|
+
await this.trackFragmentIndexLoader.taskComplete;
|
|
33
|
+
await this.initSegmentsLoader.taskComplete;
|
|
34
|
+
await this.seekTask.taskComplete;
|
|
35
|
+
await this.fetchSeekTask.taskComplete;
|
|
36
|
+
await this.videoAssetTask.taskComplete;
|
|
37
|
+
this.rootTimegroup?.requestUpdate();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { EFAudio } from "./EFAudio";
|
|
2
|
+
import { LitElement, type PropertyValueMap, html, css } from "lit";
|
|
3
|
+
import { Task } from "@lit/task";
|
|
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 { FetchMixin } from "./FetchMixin";
|
|
9
|
+
import { EFSourceMixin } from "./EFSourceMixin";
|
|
10
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
|
|
11
|
+
|
|
12
|
+
interface Word {
|
|
13
|
+
text: string;
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
confidence: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Segment {
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
text: string;
|
|
23
|
+
confidence: number;
|
|
24
|
+
words: Word[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Caption {
|
|
28
|
+
text: string;
|
|
29
|
+
segments: Segment[];
|
|
30
|
+
language: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@customElement("ef-captions-active-word")
|
|
34
|
+
export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
35
|
+
static styles = [
|
|
36
|
+
css`
|
|
37
|
+
:host {
|
|
38
|
+
display: inline-block;
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
return html`${this.wordText}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@property({ type: Number, attribute: false })
|
|
48
|
+
wordStartMs = 0;
|
|
49
|
+
|
|
50
|
+
@property({ type: Number, attribute: false })
|
|
51
|
+
wordEndMs = 0;
|
|
52
|
+
|
|
53
|
+
@property({ type: String, attribute: false })
|
|
54
|
+
wordText = "";
|
|
55
|
+
|
|
56
|
+
get startTimeMs() {
|
|
57
|
+
return this.wordStartMs || 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get durationMs(): number {
|
|
61
|
+
return this.wordEndMs - this.wordStartMs;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@customElement("ef-captions")
|
|
66
|
+
export class EFCaptions extends EFSourceMixin(
|
|
67
|
+
EFTemporal(FetchMixin(LitElement)),
|
|
68
|
+
{ assetType: "caption_files" },
|
|
69
|
+
) {
|
|
70
|
+
static styles = [
|
|
71
|
+
css`
|
|
72
|
+
:host {
|
|
73
|
+
display: block;
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
@property({ type: String, attribute: "target" })
|
|
79
|
+
target = null;
|
|
80
|
+
|
|
81
|
+
@property({ attribute: "word-style" })
|
|
82
|
+
wordStyle = "";
|
|
83
|
+
|
|
84
|
+
activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
|
|
85
|
+
|
|
86
|
+
captionsPath() {
|
|
87
|
+
const targetSrc = this.targetElement.src;
|
|
88
|
+
if (targetSrc.startsWith("editframe://") || targetSrc.startsWith("http")) {
|
|
89
|
+
return targetSrc.replace("isobmff", "caption");
|
|
90
|
+
}
|
|
91
|
+
return `/@ef-captions/${targetSrc}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected md5SumLoader = new Task(this, {
|
|
95
|
+
autoRun: false,
|
|
96
|
+
args: () => [this.target] as const,
|
|
97
|
+
task: async ([], { signal }) => {
|
|
98
|
+
const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
|
|
99
|
+
const response = await fetch(md5Path, { method: "HEAD", signal });
|
|
100
|
+
return response.headers.get("etag") ?? undefined;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
private captionsDataTask = new Task(this, {
|
|
105
|
+
autoRun: EF_INTERACTIVE,
|
|
106
|
+
args: () => [this.captionsPath(), this.fetch] as const,
|
|
107
|
+
task: async ([captionsPath, fetch], { signal }) => {
|
|
108
|
+
const response = await fetch(captionsPath, { signal });
|
|
109
|
+
return response.json() as any as Caption;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
frameTask = new Task(this, {
|
|
114
|
+
autoRun: EF_INTERACTIVE,
|
|
115
|
+
args: () => [this.captionsDataTask.status],
|
|
116
|
+
task: async () => {
|
|
117
|
+
await this.captionsDataTask.taskComplete;
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
connectedCallback() {
|
|
122
|
+
super.connectedCallback();
|
|
123
|
+
if (this.targetElement) {
|
|
124
|
+
new CrossUpdateController(this.targetElement, this);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
return this.captionsDataTask.render({
|
|
130
|
+
pending: () => html`<div>Generating captions data...</div>`,
|
|
131
|
+
error: () => html`<div>🚫 Error generating captions data</div>`,
|
|
132
|
+
complete: () => html`<slot></slot>`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected updated(
|
|
137
|
+
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
138
|
+
): void {
|
|
139
|
+
this.updateActiveWord();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
updateActiveWord() {
|
|
143
|
+
const caption = this.captionsDataTask.value;
|
|
144
|
+
if (!caption) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const words: string[] = [];
|
|
148
|
+
let startMs = 0;
|
|
149
|
+
let endMs = 0;
|
|
150
|
+
for (const segment of caption.segments) {
|
|
151
|
+
if (
|
|
152
|
+
this.targetElement.ownCurrentTimeMs >= segment.start * 1000 &&
|
|
153
|
+
this.targetElement.ownCurrentTimeMs <= segment.end * 1000
|
|
154
|
+
) {
|
|
155
|
+
for (const word of segment.words) {
|
|
156
|
+
if (
|
|
157
|
+
this.targetElement.ownCurrentTimeMs >= word.start * 1000 &&
|
|
158
|
+
this.targetElement.ownCurrentTimeMs <= word.end * 1000
|
|
159
|
+
) {
|
|
160
|
+
words.push(word.text);
|
|
161
|
+
startMs = word.start * 1000;
|
|
162
|
+
endMs = word.end * 1000;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const container of Array.from(this.activeWordContainers)) {
|
|
168
|
+
container.wordText = words.join(" ");
|
|
169
|
+
container.wordStartMs = startMs;
|
|
170
|
+
container.wordEndMs = endMs;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get targetElement() {
|
|
175
|
+
const target = document.querySelector(this.getAttribute("target") ?? "");
|
|
176
|
+
if (target instanceof EFAudio || target instanceof EFVideo) {
|
|
177
|
+
return target;
|
|
178
|
+
}
|
|
179
|
+
throw new Error("Invalid target, must be an EFAudio or EFVideo element");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
declare global {
|
|
184
|
+
interface HTMLElementTagNameMap {
|
|
185
|
+
"ef-captions": EFCaptions;
|
|
186
|
+
"ef-captions-active-word": EFCaptionsActiveWord;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Task } from "@lit/task";
|
|
2
|
+
import { LitElement, html, css } from "lit";
|
|
3
|
+
import { customElement } from "lit/decorators.js";
|
|
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";
|
|
8
|
+
|
|
9
|
+
@customElement("ef-image")
|
|
10
|
+
export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
|
|
11
|
+
assetType: "image_files",
|
|
12
|
+
}) {
|
|
13
|
+
static styles = [
|
|
14
|
+
css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
canvas {
|
|
19
|
+
display: block;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
object-fit: fill;
|
|
23
|
+
object-position: center;
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
imageRef = createRef<HTMLImageElement>();
|
|
29
|
+
canvasRef = createRef<HTMLCanvasElement>();
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
return html`<canvas ${ref(this.canvasRef)}></canvas>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assetPath() {
|
|
36
|
+
if (this.src.startsWith("editframe://") || this.src.startsWith("http")) {
|
|
37
|
+
return this.src;
|
|
38
|
+
}
|
|
39
|
+
return `/@ef-image/${this.src}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fetchImage = new Task(this, {
|
|
43
|
+
autoRun: EF_INTERACTIVE,
|
|
44
|
+
args: () => [this.assetPath(), this.fetch] as const,
|
|
45
|
+
task: async ([assetPath, fetch], { signal }) => {
|
|
46
|
+
const response = await fetch(assetPath, { signal });
|
|
47
|
+
const image = new Image();
|
|
48
|
+
image.src = URL.createObjectURL(await response.blob());
|
|
49
|
+
await new Promise((resolve) => {
|
|
50
|
+
image.onload = resolve;
|
|
51
|
+
});
|
|
52
|
+
if (!this.canvasRef.value) throw new Error("Canvas not ready");
|
|
53
|
+
const ctx = this.canvasRef.value.getContext("2d");
|
|
54
|
+
if (!ctx) throw new Error("Canvas 2d context not ready");
|
|
55
|
+
this.canvasRef.value.width = image.width;
|
|
56
|
+
this.canvasRef.value.height = image.height;
|
|
57
|
+
ctx.drawImage(image, 0, 0);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
frameTask = new Task(this, {
|
|
62
|
+
autoRun: EF_INTERACTIVE,
|
|
63
|
+
args: () => [this.fetchImage.status] as const,
|
|
64
|
+
task: async () => {
|
|
65
|
+
await this.fetchImage.taskComplete;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|