@editframe/elements 0.5.0-beta.6 → 0.5.0-beta.8
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/elements/src/EF_FRAMEGEN.mjs +130 -0
- package/dist/elements/src/EF_INTERACTIVE.mjs +4 -0
- package/dist/elements/{elements → src/elements}/EFAudio.mjs +20 -0
- package/dist/elements/{elements → src/elements}/EFCaptions.mjs +3 -0
- package/dist/elements/{elements → src/elements}/EFImage.mjs +15 -3
- package/dist/elements/{elements → src/elements}/EFMedia.mjs +81 -4
- package/dist/elements/{elements → src/elements}/EFTemporal.mjs +29 -1
- package/dist/elements/{elements → src/elements}/EFTimegroup.mjs +124 -0
- package/dist/elements/{elements → src/elements}/EFVideo.mjs +10 -0
- package/dist/elements/{elements → src/elements}/EFWaveform.mjs +41 -24
- package/dist/elements/{elements.mjs → src/elements.mjs} +2 -1
- package/dist/elements/{gui → src/gui}/EFFilmstrip.mjs +3 -2
- package/dist/elements/{gui → src/gui}/EFWorkbench.mjs +51 -63
- package/dist/elements/{gui → src/gui}/TWMixin.css.mjs +1 -1
- package/dist/style.css +3 -0
- package/dist/util/awaitAnimationFrame.mjs +11 -0
- package/docker-compose.yaml +17 -0
- package/package.json +2 -2
- package/src/EF_FRAMEGEN.ts +208 -0
- package/src/EF_INTERACTIVE.ts +2 -0
- package/src/elements/CrossUpdateController.ts +18 -0
- package/src/elements/EFAudio.ts +42 -0
- package/src/elements/EFCaptions.ts +202 -0
- package/src/elements/EFImage.ts +70 -0
- package/src/elements/EFMedia.ts +395 -0
- package/src/elements/EFSourceMixin.ts +57 -0
- package/src/elements/EFTemporal.ts +246 -0
- package/src/elements/EFTimegroup.browsertest.ts +360 -0
- package/src/elements/EFTimegroup.ts +394 -0
- package/src/elements/EFTimeline.ts +13 -0
- package/src/elements/EFVideo.ts +114 -0
- package/src/elements/EFWaveform.ts +407 -0
- package/src/elements/FetchMixin.ts +18 -0
- package/src/elements/TimegroupController.ts +25 -0
- package/src/elements/buildLitFixture.ts +13 -0
- package/src/elements/durationConverter.ts +6 -0
- package/src/elements/parseTimeToMs.ts +10 -0
- package/src/elements/util.ts +24 -0
- package/src/gui/EFFilmstrip.ts +702 -0
- package/src/gui/EFWorkbench.ts +242 -0
- package/src/gui/TWMixin.css +3 -0
- package/src/gui/TWMixin.ts +27 -0
- package/src/util.d.ts +1 -0
- package/dist/elements/elements.css.mjs +0 -1
- /package/dist/elements/{elements → src/elements}/CrossUpdateController.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/EFSourceMixin.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/EFTimeline.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/FetchMixin.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/TimegroupController.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/durationConverter.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/parseTimeToMs.mjs +0 -0
- /package/dist/elements/{elements → src/elements}/util.mjs +0 -0
- /package/dist/elements/{gui → src/gui}/TWMixin.mjs +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { VideoRenderOptions } from "services/render/app/RenderOptions";
|
|
2
|
+
import {
|
|
3
|
+
deepGetElementsWithFrameTasks,
|
|
4
|
+
shallowGetTimegroups,
|
|
5
|
+
} from "./elements/EFTemporal";
|
|
6
|
+
import { awaitAnimationFrame } from "@/util/awaitAnimationFrame";
|
|
7
|
+
import { awaitMicrotask } from "@/util/awaitMicrotask";
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
EF_FRAMEGEN?: {
|
|
12
|
+
onRender: (
|
|
13
|
+
callback: (
|
|
14
|
+
renderId: string,
|
|
15
|
+
traceCarrier: unknown,
|
|
16
|
+
renderOptions: VideoRenderOptions,
|
|
17
|
+
) => void,
|
|
18
|
+
) => void;
|
|
19
|
+
onBegin: (
|
|
20
|
+
callback: (
|
|
21
|
+
traceCarrier: unknown,
|
|
22
|
+
frame: number,
|
|
23
|
+
isLast: boolean,
|
|
24
|
+
) => void,
|
|
25
|
+
) => void;
|
|
26
|
+
frameReady: (
|
|
27
|
+
renderId: string,
|
|
28
|
+
frameNumber: number,
|
|
29
|
+
samples: ArrayBufferLike,
|
|
30
|
+
) => void;
|
|
31
|
+
onPaint: (callback: () => void) => void;
|
|
32
|
+
didPaint: (renderId: string) => void;
|
|
33
|
+
end: (renderId: string) => void;
|
|
34
|
+
error: (renderId: string, error: string) => void;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class TriggerCanvas {
|
|
40
|
+
private canvas: HTMLCanvasElement;
|
|
41
|
+
private ctx: CanvasRenderingContext2D;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.canvas = document.createElement("canvas");
|
|
45
|
+
this.canvas.width = 1;
|
|
46
|
+
this.canvas.height = 1;
|
|
47
|
+
Object.assign(this.canvas.style, {
|
|
48
|
+
position: "absolute",
|
|
49
|
+
top: "0px",
|
|
50
|
+
left: "0px",
|
|
51
|
+
width: `1px`,
|
|
52
|
+
height: `1px`,
|
|
53
|
+
zIndex: "100000",
|
|
54
|
+
});
|
|
55
|
+
document.body.prepend(this.canvas);
|
|
56
|
+
this.ctx = this.canvas.getContext("2d")!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
trigger() {
|
|
60
|
+
console.log("Triggering");
|
|
61
|
+
this.ctx.fillStyle = "rgba(50, 0, 0, .8)";
|
|
62
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
63
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (window.EF_FRAMEGEN !== undefined) {
|
|
68
|
+
const EF_FRAMEGEN = window.EF_FRAMEGEN;
|
|
69
|
+
EF_FRAMEGEN.onRender((renderId, traceCarrier, renderOptions) => {
|
|
70
|
+
const crashOnUnhandledError = (error: string) => {
|
|
71
|
+
EF_FRAMEGEN.error(renderId, error);
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener("error", (error) => {
|
|
74
|
+
console.warn("Crashing due to unhandled error", error);
|
|
75
|
+
crashOnUnhandledError(error.message);
|
|
76
|
+
});
|
|
77
|
+
window.addEventListener("unhandledrejection", (error) => {
|
|
78
|
+
console.warn("Crashing due to unhandled rejection", error);
|
|
79
|
+
crashOnUnhandledError(error.reason);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const triggerCanvas = new TriggerCanvas();
|
|
83
|
+
EF_FRAMEGEN.onPaint(async () => {
|
|
84
|
+
// Calls clearRect on the canvas to force the compositor to emit a frame.
|
|
85
|
+
triggerCanvas.trigger();
|
|
86
|
+
|
|
87
|
+
// We must wait for 2 frames to ensure offscreen render capture doesn't capture
|
|
88
|
+
// a stale frame. This is terrible. Fixing this requires contribiting to electron.js
|
|
89
|
+
// On the positive side, were' running at 240fps so it only burns 8ms max.
|
|
90
|
+
// (electron can't go faster thon 240fps)
|
|
91
|
+
// In practice it's less because we're already some ms into the next frame budget.
|
|
92
|
+
await awaitAnimationFrame();
|
|
93
|
+
await awaitAnimationFrame();
|
|
94
|
+
|
|
95
|
+
EF_FRAMEGEN.didPaint(renderId);
|
|
96
|
+
});
|
|
97
|
+
const workbench = document.querySelector("ef-workbench")!;
|
|
98
|
+
workbench.rendering = true;
|
|
99
|
+
const timegroups = shallowGetTimegroups(workbench);
|
|
100
|
+
const temporals = deepGetElementsWithFrameTasks(workbench);
|
|
101
|
+
const firstGroup = timegroups[0];
|
|
102
|
+
if (!firstGroup) {
|
|
103
|
+
throw new Error("No temporal elements found");
|
|
104
|
+
}
|
|
105
|
+
firstGroup.currentTimeMs = renderOptions.encoderOptions.fromMs;
|
|
106
|
+
|
|
107
|
+
const frameDurationMs = 1000 / renderOptions.encoderOptions.video.framerate;
|
|
108
|
+
|
|
109
|
+
const initialBusyTasks = Promise.all(
|
|
110
|
+
temporals
|
|
111
|
+
// .filter((temporal) => temporal.frameTask.status < TaskStatus.COMPLETE)
|
|
112
|
+
.map((temporal) => temporal.frameTask)
|
|
113
|
+
.map((task) => task.taskComplete),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const frameBox = document.createElement("div");
|
|
117
|
+
Object.assign(frameBox.style, {
|
|
118
|
+
width: "200px",
|
|
119
|
+
height: "100px",
|
|
120
|
+
font: "30px Arial",
|
|
121
|
+
backgroundColor: "white",
|
|
122
|
+
position: "absolute",
|
|
123
|
+
top: "0px",
|
|
124
|
+
left: "0px",
|
|
125
|
+
zIndex: "100000",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let time = 0;
|
|
129
|
+
// document.body.prepend(frameBox);
|
|
130
|
+
// TODO: terminate if no workbench found
|
|
131
|
+
console.log("onRender", renderId, traceCarrier, renderOptions);
|
|
132
|
+
|
|
133
|
+
const audioBufferPromise = firstGroup.renderAudio(
|
|
134
|
+
renderOptions.encoderOptions.alignedFromUs / 1000,
|
|
135
|
+
renderOptions.encoderOptions.alignedToUs / 1000,
|
|
136
|
+
// renderOptions.encoderOptions.fromMs,
|
|
137
|
+
// renderOptions.encoderOptions.toMs,
|
|
138
|
+
);
|
|
139
|
+
EF_FRAMEGEN.onBegin(async (traceCarrier, frame, isLast) => {
|
|
140
|
+
time = firstGroup.currentTimeMs =
|
|
141
|
+
renderOptions.encoderOptions.fromMs + frame * frameDurationMs;
|
|
142
|
+
console.log("FRAME #", frame);
|
|
143
|
+
frameBox.innerHTML = `
|
|
144
|
+
<div>Frame #${frame}</div>
|
|
145
|
+
<div>${time.toFixed(4)}</div>
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
await initialBusyTasks;
|
|
149
|
+
|
|
150
|
+
console.log("TIME", time.toFixed(4));
|
|
151
|
+
await awaitMicrotask();
|
|
152
|
+
console.log("After microtask");
|
|
153
|
+
|
|
154
|
+
const now = performance.now();
|
|
155
|
+
console.log(`frame:${frame} Awaiting busyTasks`);
|
|
156
|
+
await Promise.all(
|
|
157
|
+
temporals
|
|
158
|
+
// .filter((temporal) => temporal.frameTask.status < TaskStatus.COMPLETE)
|
|
159
|
+
.map((temporal) => {
|
|
160
|
+
console.log(
|
|
161
|
+
"Awaiting",
|
|
162
|
+
temporal.tagName,
|
|
163
|
+
temporal.frameTask.status,
|
|
164
|
+
temporal.frameTask.taskComplete,
|
|
165
|
+
);
|
|
166
|
+
return temporal.frameTask;
|
|
167
|
+
})
|
|
168
|
+
.map((task) => task.taskComplete),
|
|
169
|
+
);
|
|
170
|
+
console.log(
|
|
171
|
+
`frame:${frame} All tasks complete ${performance.now() - now}ms`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await awaitAnimationFrame();
|
|
175
|
+
await awaitAnimationFrame();
|
|
176
|
+
|
|
177
|
+
// Trigger a small canvas paint to force the compositor to emit a frame.
|
|
178
|
+
triggerCanvas.trigger();
|
|
179
|
+
|
|
180
|
+
if (isLast) {
|
|
181
|
+
// Currently we emit the audio in one belch at the end of the render.
|
|
182
|
+
// This is not ideal, but it's the simplest thing that could possibly work.
|
|
183
|
+
// We could either emit it slices, or in parallel with the video.
|
|
184
|
+
// But in any case, it's fine for now.
|
|
185
|
+
const renderedAudio = await audioBufferPromise;
|
|
186
|
+
|
|
187
|
+
const channelCount = renderedAudio.numberOfChannels;
|
|
188
|
+
|
|
189
|
+
const interleavedSamples = new Float32Array(
|
|
190
|
+
channelCount * renderedAudio.length,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < renderedAudio.length; i++) {
|
|
194
|
+
for (let j = 0; j < channelCount; j++) {
|
|
195
|
+
interleavedSamples.set(
|
|
196
|
+
renderedAudio.getChannelData(j).slice(i, i + 1),
|
|
197
|
+
i * channelCount + j,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
EF_FRAMEGEN.frameReady(renderId, frame, interleavedSamples.buffer);
|
|
203
|
+
} else {
|
|
204
|
+
EF_FRAMEGEN.frameReady(renderId, frame, new Float32Array(0).buffer);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LitElement, ReactiveController, ReactiveControllerHost } from "lit";
|
|
2
|
+
|
|
3
|
+
export class CrossUpdateController implements ReactiveController {
|
|
4
|
+
constructor(
|
|
5
|
+
private host: ReactiveControllerHost,
|
|
6
|
+
private target: LitElement,
|
|
7
|
+
) {
|
|
8
|
+
this.host.addController(this);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
hostUpdate(): void {
|
|
12
|
+
this.target.requestUpdate();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
remove(): void {
|
|
16
|
+
this.host.removeController(this);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
console.log("EFAudio frameTask", this.ownCurrentTimeMs);
|
|
33
|
+
await this.trackFragmentIndexLoader.taskComplete;
|
|
34
|
+
await this.initSegmentsLoader.taskComplete;
|
|
35
|
+
await this.seekTask.taskComplete;
|
|
36
|
+
await this.fetchSeekTask.taskComplete;
|
|
37
|
+
await this.videoAssetTask.taskComplete;
|
|
38
|
+
console.log("EFAudio frameTask complete", this.ownCurrentTimeMs);
|
|
39
|
+
this.rootTimegroup?.requestUpdate();
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { EFAudio } from "./EFAudio";
|
|
2
|
+
import { LitElement, 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 src = this.targetElement.getAttribute("src");
|
|
88
|
+
if (src?.startsWith("http")) {
|
|
89
|
+
return src.replace("isobmff", "caption");
|
|
90
|
+
}
|
|
91
|
+
return `/@ef-captions/${this.targetElement.getAttribute("src") ?? ""}`;
|
|
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.getAttribute("src") ?? ""}`;
|
|
99
|
+
const response = await fetch(md5Path, { method: "HEAD", signal });
|
|
100
|
+
return response.headers.get("etag") ?? undefined;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
productionSrc() {
|
|
105
|
+
if (!this.md5SumLoader.value) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`MD5 sum not available for ${this}. Cannot generate production URL`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return `http://localhost:3000/api/video2/caption_files/${this.md5SumLoader.value}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// get requiredAssets() {
|
|
114
|
+
// return { [this.md5SumLoader.value]: [this.captionsPath()] };
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
private captionsDataTask = new Task(this, {
|
|
118
|
+
autoRun: EF_INTERACTIVE,
|
|
119
|
+
args: () => [this.captionsPath(), this.fetch] as const,
|
|
120
|
+
task: async ([captionsPath, fetch], { signal }) => {
|
|
121
|
+
const response = await fetch(captionsPath, { signal });
|
|
122
|
+
return response.json() as any as Caption;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
frameTask = new Task(this, {
|
|
127
|
+
autoRun: EF_INTERACTIVE,
|
|
128
|
+
args: () => [this.captionsDataTask.status],
|
|
129
|
+
task: async () => {
|
|
130
|
+
await this.captionsDataTask.taskComplete;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
connectedCallback() {
|
|
135
|
+
super.connectedCallback();
|
|
136
|
+
if (this.targetElement) {
|
|
137
|
+
new CrossUpdateController(this.targetElement, this);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render() {
|
|
142
|
+
return this.captionsDataTask.render({
|
|
143
|
+
pending: () => html`<div>Generating captions data...</div>`,
|
|
144
|
+
error: () => html`<div>🚫 Error generating captions data</div>`,
|
|
145
|
+
complete: () => html`<slot></slot>`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected updated(
|
|
150
|
+
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
151
|
+
): void {
|
|
152
|
+
this.updateActiveWord();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
updateActiveWord() {
|
|
156
|
+
const caption = this.captionsDataTask.value;
|
|
157
|
+
if (!caption) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const words: string[] = [];
|
|
161
|
+
let startMs: number;
|
|
162
|
+
let endMs: number;
|
|
163
|
+
caption.segments.forEach((segment) => {
|
|
164
|
+
if (
|
|
165
|
+
this.targetElement.ownCurrentTimeMs >= segment.start * 1000 &&
|
|
166
|
+
this.targetElement.ownCurrentTimeMs <= segment.end * 1000
|
|
167
|
+
) {
|
|
168
|
+
return segment.words.map((word) => {
|
|
169
|
+
if (
|
|
170
|
+
this.targetElement.ownCurrentTimeMs >= word.start * 1000 &&
|
|
171
|
+
this.targetElement.ownCurrentTimeMs <= word.end * 1000
|
|
172
|
+
) {
|
|
173
|
+
words.push(word.text);
|
|
174
|
+
startMs = word.start * 1000;
|
|
175
|
+
endMs = word.end * 1000;
|
|
176
|
+
} else {
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
Array.from(this.activeWordContainers).forEach((container) => {
|
|
182
|
+
container.wordText = words.join(" ");
|
|
183
|
+
container.wordStartMs = startMs;
|
|
184
|
+
container.wordEndMs = endMs;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get targetElement() {
|
|
189
|
+
const target = document.querySelector(this.getAttribute("target") ?? "");
|
|
190
|
+
if (target instanceof EFAudio || target instanceof EFVideo) {
|
|
191
|
+
return target;
|
|
192
|
+
}
|
|
193
|
+
throw new Error("Invalid target, must be an EFAudio or EFVideo element");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
declare global {
|
|
198
|
+
interface HTMLElementTagNameMap {
|
|
199
|
+
"ef-captions": EFCaptions;
|
|
200
|
+
"ef-captions-active-word": EFCaptionsActiveWord;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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("http")) {
|
|
37
|
+
return this.src;
|
|
38
|
+
}
|
|
39
|
+
return `/@ef-image/${this.src}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// get requiredAssets() {
|
|
43
|
+
// return { [this.md5SumLoader.value]: [this.assetPath()] };
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
fetchImage = new Task(this, {
|
|
47
|
+
autoRun: EF_INTERACTIVE,
|
|
48
|
+
args: () => [this.assetPath(), this.fetch] as const,
|
|
49
|
+
task: async ([assetPath, fetch], { signal }) => {
|
|
50
|
+
const response = await fetch(assetPath, { signal });
|
|
51
|
+
const image = new Image();
|
|
52
|
+
image.src = URL.createObjectURL(await response.blob());
|
|
53
|
+
await new Promise((resolve) => {
|
|
54
|
+
image.onload = resolve;
|
|
55
|
+
});
|
|
56
|
+
this.canvasRef.value!.width = image.width;
|
|
57
|
+
this.canvasRef.value!.height = image.height;
|
|
58
|
+
const ctx = this.canvasRef.value!.getContext("2d")!;
|
|
59
|
+
ctx.drawImage(image, 0, 0);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
frameTask = new Task(this, {
|
|
64
|
+
autoRun: EF_INTERACTIVE,
|
|
65
|
+
args: () => [this.fetchImage.status] as const,
|
|
66
|
+
task: async () => {
|
|
67
|
+
await this.fetchImage.taskComplete;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|