@editframe/elements 0.7.0-beta.9 → 0.8.0-beta.2
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 +64 -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 +356 -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 +236 -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 +39 -11
- 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 +256 -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
|
@@ -0,0 +1,256 @@
|
|
|
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.#URLTokens[url]) {
|
|
55
|
+
this.#URLTokens[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()).token;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const urlToken = await this.#URLTokens[url];
|
|
69
|
+
|
|
70
|
+
Object.assign(init.headers, {
|
|
71
|
+
authorization: `Bearer ${urlToken}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return fetch(url, init);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
#URLTokens: Record<string, Promise<string>> = {};
|
|
79
|
+
|
|
80
|
+
@property({ type: String })
|
|
81
|
+
signingURL?: string;
|
|
82
|
+
|
|
83
|
+
@property({ type: String })
|
|
84
|
+
apiToken?: string;
|
|
85
|
+
|
|
86
|
+
@provide({ context: apiHostContext })
|
|
87
|
+
@property({ type: String })
|
|
88
|
+
apiHost = "";
|
|
89
|
+
|
|
90
|
+
@provide({ context: playingContext })
|
|
91
|
+
@property({ type: Boolean, reflect: true })
|
|
92
|
+
playing = false;
|
|
93
|
+
|
|
94
|
+
@property({ type: Boolean, reflect: true })
|
|
95
|
+
loop = false;
|
|
96
|
+
|
|
97
|
+
@state()
|
|
98
|
+
stageScale = 1;
|
|
99
|
+
|
|
100
|
+
@property({ type: Boolean })
|
|
101
|
+
rendering = false;
|
|
102
|
+
|
|
103
|
+
@state()
|
|
104
|
+
currentTimeMs = 0;
|
|
105
|
+
|
|
106
|
+
stageRef = createRef<HTMLDivElement>();
|
|
107
|
+
canvasRef = createRef<HTMLSlotElement>();
|
|
108
|
+
|
|
109
|
+
setStageScale = () => {
|
|
110
|
+
if (this.isConnected && !this.rendering) {
|
|
111
|
+
const canvasElement = this.canvasRef.value;
|
|
112
|
+
const stageElement = this.stageRef.value;
|
|
113
|
+
if (stageElement && canvasElement) {
|
|
114
|
+
// Determine the appropriate scale factor to make the canvas fit into
|
|
115
|
+
// it's parent element.
|
|
116
|
+
const stageWidth = stageElement.clientWidth;
|
|
117
|
+
const stageHeight = stageElement.clientHeight;
|
|
118
|
+
const canvasWidth = canvasElement.clientWidth;
|
|
119
|
+
const canvasHeight = canvasElement.clientHeight;
|
|
120
|
+
const stageRatio = stageWidth / stageHeight;
|
|
121
|
+
const canvasRatio = canvasWidth / canvasHeight;
|
|
122
|
+
if (stageRatio > canvasRatio) {
|
|
123
|
+
const scale = stageHeight / canvasHeight;
|
|
124
|
+
if (this.stageScale !== scale) {
|
|
125
|
+
canvasElement.style.transform = `scale(${scale})`;
|
|
126
|
+
}
|
|
127
|
+
this.stageScale = scale;
|
|
128
|
+
} else {
|
|
129
|
+
const scale = stageWidth / canvasWidth;
|
|
130
|
+
if (this.stageScale !== scale) {
|
|
131
|
+
canvasElement.style.transform = `scale(${scale})`;
|
|
132
|
+
}
|
|
133
|
+
this.stageScale = scale;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (this.isConnected) {
|
|
138
|
+
requestAnimationFrame(this.setStageScale);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
connectedCallback(): void {
|
|
143
|
+
super.connectedCallback();
|
|
144
|
+
// Preferrably we would use a resizeObserver, but it is difficult to get the first resize
|
|
145
|
+
// timed correctly. So we use requestAnimationFrame as a stop-gap.
|
|
146
|
+
requestAnimationFrame(this.setStageScale);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
update(changedProperties: Map<string | number | symbol, unknown>) {
|
|
150
|
+
if (changedProperties.has("playing")) {
|
|
151
|
+
if (this.playing) {
|
|
152
|
+
this.#startPlayback();
|
|
153
|
+
} else {
|
|
154
|
+
this.#stopPlayback();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
|
|
159
|
+
if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
|
|
160
|
+
this.targetTimegroup.currentTimeMs = this.currentTimeMs;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
super.update(changedProperties);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get targetTimegroup() {
|
|
167
|
+
return this.querySelector("ef-timegroup");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#playbackAudioContext: AudioContext | null = null;
|
|
171
|
+
#playbackAnimationFrameRequest: number | null = null;
|
|
172
|
+
#AUDIO_PLAYBACK_SLICE_MS = 1000;
|
|
173
|
+
|
|
174
|
+
#syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
|
|
175
|
+
this.currentTimeMs =
|
|
176
|
+
startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
|
|
177
|
+
this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
|
|
178
|
+
this.#syncPlayheadToAudioContext(target, startMs);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async #stopPlayback() {
|
|
183
|
+
if (this.#playbackAudioContext) {
|
|
184
|
+
if (this.#playbackAudioContext.state !== "closed") {
|
|
185
|
+
await this.#playbackAudioContext.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (this.#playbackAnimationFrameRequest) {
|
|
189
|
+
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
190
|
+
}
|
|
191
|
+
this.#playbackAudioContext = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async #startPlayback() {
|
|
195
|
+
await this.#stopPlayback();
|
|
196
|
+
const timegroup = this.targetTimegroup;
|
|
197
|
+
if (!timegroup) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let currentMs = timegroup.currentTimeMs;
|
|
202
|
+
let bufferCount = 0;
|
|
203
|
+
this.#playbackAudioContext = new AudioContext({
|
|
204
|
+
latencyHint: "playback",
|
|
205
|
+
});
|
|
206
|
+
if (this.#playbackAnimationFrameRequest) {
|
|
207
|
+
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
208
|
+
}
|
|
209
|
+
this.#syncPlayheadToAudioContext(timegroup, currentMs);
|
|
210
|
+
const playbackContext = this.#playbackAudioContext;
|
|
211
|
+
await playbackContext.suspend();
|
|
212
|
+
|
|
213
|
+
const fillBuffer = async () => {
|
|
214
|
+
if (bufferCount > 1) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const canFillBuffer = await queueBufferSource();
|
|
218
|
+
if (canFillBuffer) {
|
|
219
|
+
fillBuffer();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const fromMs = currentMs;
|
|
224
|
+
const toMs = timegroup.endTimeMs;
|
|
225
|
+
|
|
226
|
+
const queueBufferSource = async () => {
|
|
227
|
+
if (currentMs >= toMs) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const startMs = currentMs;
|
|
231
|
+
const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
232
|
+
currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
233
|
+
const audioBuffer = await timegroup.renderAudio(startMs, endMs);
|
|
234
|
+
bufferCount++;
|
|
235
|
+
const source = playbackContext.createBufferSource();
|
|
236
|
+
source.buffer = audioBuffer;
|
|
237
|
+
source.connect(playbackContext.destination);
|
|
238
|
+
source.start((startMs - fromMs) / 1000);
|
|
239
|
+
source.onended = () => {
|
|
240
|
+
bufferCount--;
|
|
241
|
+
if (endMs >= toMs) {
|
|
242
|
+
this.playing = false;
|
|
243
|
+
} else {
|
|
244
|
+
fillBuffer();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
await fillBuffer();
|
|
251
|
+
await playbackContext.resume();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return ContextElement as Constructor<ContextMixinInterface> & T;
|
|
256
|
+
}
|
package/src/gui/EFFilmstrip.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EFTimegroup } from "../elements/EFTimegroup";
|
|
2
1
|
import {
|
|
3
2
|
LitElement,
|
|
4
3
|
html,
|
|
@@ -14,19 +13,25 @@ import {
|
|
|
14
13
|
eventOptions,
|
|
15
14
|
state,
|
|
16
15
|
} from "lit/decorators.js";
|
|
16
|
+
import { consume } from "@lit/context";
|
|
17
17
|
import { styleMap } from "lit/directives/style-map.js";
|
|
18
18
|
import { ref, createRef } from "lit/directives/ref.js";
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { TWMixin } from "./TWMixin";
|
|
29
|
-
import { msToTimeCode } from "
|
|
19
|
+
|
|
20
|
+
import { EFImage } from "../elements/EFImage.ts";
|
|
21
|
+
import { EFAudio } from "../elements/EFAudio.ts";
|
|
22
|
+
import { EFVideo } from "../elements/EFVideo.ts";
|
|
23
|
+
import { EFCaptions, EFCaptionsActiveWord } from "../elements/EFCaptions.ts";
|
|
24
|
+
import { EFWaveform } from "../elements/EFWaveform.ts";
|
|
25
|
+
import { EFTimegroup } from "../elements/EFTimegroup.ts";
|
|
26
|
+
import type { TemporalMixinInterface } from "../elements/EFTemporal.ts";
|
|
27
|
+
import { TimegroupController } from "../elements/TimegroupController.ts";
|
|
28
|
+
import { TWMixin } from "./TWMixin.ts";
|
|
29
|
+
import { msToTimeCode } from "../msToTimeCode.ts";
|
|
30
|
+
import { focusedElementContext } from "./focusedElementContext.ts";
|
|
31
|
+
import { type FocusContext, focusContext } from "./focusContext.ts";
|
|
32
|
+
import { playingContext } from "./playingContext.ts";
|
|
33
|
+
import type { EFWorkbench } from "./EFWorkbench.ts";
|
|
34
|
+
import type { EFPreview } from "./EFPreview.ts";
|
|
30
35
|
|
|
31
36
|
class ElementFilmstripController implements ReactiveController {
|
|
32
37
|
constructor(
|
|
@@ -68,14 +73,14 @@ class FilmstripItem extends TWMixin(LitElement) {
|
|
|
68
73
|
@consume({ context: focusContext, subscribe: true })
|
|
69
74
|
focusContext?: FocusContext;
|
|
70
75
|
|
|
71
|
-
@consume({ context:
|
|
76
|
+
@consume({ context: focusedElementContext, subscribe: true })
|
|
72
77
|
focusedElement?: HTMLElement | null;
|
|
73
78
|
|
|
74
79
|
get isFocused() {
|
|
75
80
|
return this.element && this.focusContext?.focusedElement === this.element;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
@property({ type:
|
|
83
|
+
@property({ type: Object, attribute: false })
|
|
79
84
|
element: TemporalMixinInterface & LitElement = new EFTimegroup();
|
|
80
85
|
|
|
81
86
|
@property({ type: Number })
|
|
@@ -254,14 +259,14 @@ export class EFHTMLFilmstrip extends FilmstripItem {
|
|
|
254
259
|
class EFHierarchyItem<
|
|
255
260
|
ElementType extends HTMLElement = HTMLElement,
|
|
256
261
|
> extends TWMixin(LitElement) {
|
|
257
|
-
@property({ type:
|
|
262
|
+
@property({ type: Object, attribute: false })
|
|
258
263
|
// @ts-expect-error This could be initialzed with any HTMLElement
|
|
259
264
|
element: ElementType = new EFTimegroup();
|
|
260
265
|
|
|
261
266
|
@consume({ context: focusContext })
|
|
262
267
|
focusContext?: FocusContext;
|
|
263
268
|
|
|
264
|
-
@consume({ context:
|
|
269
|
+
@consume({ context: focusedElementContext, subscribe: true })
|
|
265
270
|
focusedElement?: HTMLElement | null;
|
|
266
271
|
|
|
267
272
|
get icon(): TemplateResult<1> | string {
|
|
@@ -485,23 +490,21 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
485
490
|
@property({ type: Number })
|
|
486
491
|
pixelsPerMs = 0.04;
|
|
487
492
|
|
|
488
|
-
@property({ type: Number })
|
|
489
|
-
currentTimeMs = 0;
|
|
490
|
-
|
|
491
|
-
@property({ type: String, attribute: "target", reflect: true })
|
|
492
|
-
targetSelector = "";
|
|
493
|
-
|
|
494
493
|
@state()
|
|
495
494
|
scrubbing = false;
|
|
496
495
|
|
|
497
496
|
@state()
|
|
498
|
-
|
|
497
|
+
timelineScrolltop = 0;
|
|
499
498
|
|
|
499
|
+
@consume({ context: playingContext, subscribe: true })
|
|
500
500
|
@state()
|
|
501
|
-
|
|
501
|
+
playing?: boolean;
|
|
502
502
|
|
|
503
503
|
timegroupController?: TimegroupController;
|
|
504
504
|
|
|
505
|
+
@state()
|
|
506
|
+
currentTimeMs = 0;
|
|
507
|
+
|
|
505
508
|
connectedCallback(): void {
|
|
506
509
|
super.connectedCallback();
|
|
507
510
|
this.#bindToTargetTimegroup();
|
|
@@ -541,7 +544,9 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
541
544
|
return;
|
|
542
545
|
}
|
|
543
546
|
event.preventDefault();
|
|
544
|
-
this
|
|
547
|
+
if (this.#contextElement) {
|
|
548
|
+
this.#contextElement.playing = !this.#contextElement.playing;
|
|
549
|
+
}
|
|
545
550
|
}
|
|
546
551
|
};
|
|
547
552
|
|
|
@@ -561,108 +566,6 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
561
566
|
}
|
|
562
567
|
}
|
|
563
568
|
|
|
564
|
-
#lastTick?: DOMHighResTimeStamp;
|
|
565
|
-
|
|
566
|
-
#playbackAudioContext: AudioContext | null = null;
|
|
567
|
-
#playbackAnimationFrameRequest: number | null = null;
|
|
568
|
-
#AUDIO_PLAYBACK_SLICE_MS = 1000;
|
|
569
|
-
|
|
570
|
-
#syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
|
|
571
|
-
target.currentTimeMs =
|
|
572
|
-
startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
|
|
573
|
-
this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
|
|
574
|
-
this.#syncPlayheadToAudioContext(target, startMs);
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async #stopPlayback() {
|
|
579
|
-
if (this.#playbackAudioContext) {
|
|
580
|
-
if (this.#playbackAudioContext.state !== "closed") {
|
|
581
|
-
await this.#playbackAudioContext.close();
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (this.#playbackAnimationFrameRequest) {
|
|
585
|
-
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
586
|
-
}
|
|
587
|
-
this.#playbackAudioContext = null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async #startPlayback() {
|
|
591
|
-
await this.#stopPlayback();
|
|
592
|
-
const timegroup = this.targetTimegroup;
|
|
593
|
-
if (!timegroup) {
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
let currentMs = timegroup.currentTimeMs;
|
|
598
|
-
let bufferCount = 0;
|
|
599
|
-
this.#playbackAudioContext = new AudioContext({
|
|
600
|
-
latencyHint: "playback",
|
|
601
|
-
});
|
|
602
|
-
if (this.#playbackAnimationFrameRequest) {
|
|
603
|
-
cancelAnimationFrame(this.#playbackAnimationFrameRequest);
|
|
604
|
-
}
|
|
605
|
-
this.#syncPlayheadToAudioContext(timegroup, currentMs);
|
|
606
|
-
const playbackContext = this.#playbackAudioContext;
|
|
607
|
-
await playbackContext.suspend();
|
|
608
|
-
|
|
609
|
-
const fillBuffer = async () => {
|
|
610
|
-
if (bufferCount > 1) {
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
const canFillBuffer = await queueBufferSource();
|
|
614
|
-
if (canFillBuffer) {
|
|
615
|
-
fillBuffer();
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const fromMs = currentMs;
|
|
620
|
-
const toMs = timegroup.endTimeMs;
|
|
621
|
-
|
|
622
|
-
const queueBufferSource = async () => {
|
|
623
|
-
if (currentMs >= toMs) {
|
|
624
|
-
return false;
|
|
625
|
-
}
|
|
626
|
-
const startMs = currentMs;
|
|
627
|
-
const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
628
|
-
currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
|
|
629
|
-
const audioBuffer = await timegroup.renderAudio(startMs, endMs);
|
|
630
|
-
bufferCount++;
|
|
631
|
-
const source = playbackContext.createBufferSource();
|
|
632
|
-
source.buffer = audioBuffer;
|
|
633
|
-
source.connect(playbackContext.destination);
|
|
634
|
-
source.start((startMs - fromMs) / 1000);
|
|
635
|
-
source.onended = () => {
|
|
636
|
-
bufferCount--;
|
|
637
|
-
if (endMs >= toMs) {
|
|
638
|
-
this.playing = false;
|
|
639
|
-
} else {
|
|
640
|
-
fillBuffer();
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
return true;
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
await fillBuffer();
|
|
647
|
-
await playbackContext.resume();
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
advancePlayhead = (tick?: DOMHighResTimeStamp) => {
|
|
651
|
-
if (this.#lastTick && tick && this.targetTimegroup) {
|
|
652
|
-
this.targetTimegroup.currentTimeMs += tick - this.#lastTick;
|
|
653
|
-
if (
|
|
654
|
-
this.targetTimegroup.currentTimeMs >= this.targetTimegroup.durationMs
|
|
655
|
-
) {
|
|
656
|
-
this.playing = false;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
this.#lastTick = tick;
|
|
660
|
-
|
|
661
|
-
if (this.playing) {
|
|
662
|
-
requestAnimationFrame(this.advancePlayhead);
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
|
|
666
569
|
@eventOptions({ capture: false })
|
|
667
570
|
scrub(e: MouseEvent) {
|
|
668
571
|
if (this.playing) {
|
|
@@ -777,14 +680,18 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
777
680
|
this.playing
|
|
778
681
|
? html`<button
|
|
779
682
|
@click=${() => {
|
|
780
|
-
this
|
|
683
|
+
if (this.#contextElement) {
|
|
684
|
+
this.#contextElement.playing = false;
|
|
685
|
+
}
|
|
781
686
|
}}
|
|
782
687
|
>
|
|
783
688
|
⏸️
|
|
784
689
|
</button>`
|
|
785
690
|
: html`<button
|
|
786
691
|
@click=${() => {
|
|
787
|
-
this
|
|
692
|
+
if (this.#contextElement) {
|
|
693
|
+
this.#contextElement.playing = true;
|
|
694
|
+
}
|
|
788
695
|
}}
|
|
789
696
|
>
|
|
790
697
|
▶️
|
|
@@ -826,17 +733,6 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
826
733
|
</div>`;
|
|
827
734
|
}
|
|
828
735
|
|
|
829
|
-
update(changedProperties: Map<string | number | symbol, unknown>) {
|
|
830
|
-
if (changedProperties.has("playing")) {
|
|
831
|
-
if (this.playing) {
|
|
832
|
-
this.#startPlayback();
|
|
833
|
-
} else {
|
|
834
|
-
this.#stopPlayback();
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
super.update(changedProperties);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
736
|
updated(changes: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
|
|
841
737
|
if (!this.targetTimegroup) {
|
|
842
738
|
return;
|
|
@@ -846,19 +742,14 @@ export class EFFilmstrip extends TWMixin(LitElement) {
|
|
|
846
742
|
this.targetTimegroup.currentTimeMs = this.currentTimeMs;
|
|
847
743
|
}
|
|
848
744
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
get #contextElement(): EFWorkbench | EFPreview | null {
|
|
748
|
+
return this.closest("ef-workbench, ef-preview") as EFWorkbench | EFPreview;
|
|
852
749
|
}
|
|
853
750
|
|
|
854
751
|
get targetTimegroup() {
|
|
855
|
-
|
|
856
|
-
const target = document.getElementById(this.getAttribute("target") ?? "");
|
|
857
|
-
if (target instanceof EFTimegroup) {
|
|
858
|
-
return target;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return undefined;
|
|
752
|
+
return this.#contextElement?.targetTimegroup;
|
|
862
753
|
}
|
|
863
754
|
}
|
|
864
755
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
import { ref } from "lit/directives/ref.js";
|
|
4
|
+
|
|
5
|
+
import { TWMixin } from "./TWMixin.ts";
|
|
6
|
+
import { ContextMixin } from "./ContextMixin.ts";
|
|
7
|
+
|
|
8
|
+
@customElement("ef-preview")
|
|
9
|
+
export class EFPreview extends ContextMixin(TWMixin(LitElement)) {
|
|
10
|
+
static styles = [
|
|
11
|
+
css`
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
render() {
|
|
21
|
+
return html`
|
|
22
|
+
<div
|
|
23
|
+
${ref(this.stageRef)}
|
|
24
|
+
class="relative grid h-full w-full place-content-center place-items-center overflow-hidden"
|
|
25
|
+
>
|
|
26
|
+
<slot
|
|
27
|
+
${ref(this.canvasRef)}
|
|
28
|
+
class="inline-block"
|
|
29
|
+
></slot>
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare global {
|
|
36
|
+
interface HTMLElementTagNameMap {
|
|
37
|
+
"ef-preview": EFPreview;
|
|
38
|
+
}
|
|
39
|
+
}
|