@editframe/elements 0.7.0-beta.9 → 0.8.0-beta.10

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.
Files changed (107) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +43 -0
  2. package/dist/EF_INTERACTIVE.d.ts +1 -0
  3. package/dist/assets/dist/EncodedAsset.js +560 -0
  4. package/dist/assets/dist/MP4File.js +170 -0
  5. package/dist/assets/dist/memoize.js +14 -0
  6. package/dist/elements/CrossUpdateController.d.ts +8 -0
  7. package/dist/elements/EFAudio.d.ts +9 -0
  8. package/dist/elements/EFCaptions.d.ts +38 -0
  9. package/dist/elements/EFImage.d.ts +13 -0
  10. package/dist/elements/EFMedia.d.ts +63 -0
  11. package/dist/elements/EFSourceMixin.d.ts +11 -0
  12. package/dist/elements/EFTemporal.d.ts +40 -0
  13. package/dist/elements/EFTimegroup.browsertest.d.ts +11 -0
  14. package/dist/elements/EFTimegroup.d.ts +36 -0
  15. package/dist/elements/EFVideo.d.ts +13 -0
  16. package/dist/elements/EFWaveform.d.ts +29 -0
  17. package/dist/elements/FetchMixin.d.ts +7 -0
  18. package/dist/elements/TimegroupController.d.ts +13 -0
  19. package/dist/elements/durationConverter.d.ts +12 -0
  20. package/dist/elements/parseTimeToMs.d.ts +1 -0
  21. package/{src/EF_FRAMEGEN.ts → dist/elements/src/EF_FRAMEGEN.js} +35 -115
  22. package/dist/elements/src/EF_INTERACTIVE.js +7 -0
  23. package/dist/elements/src/elements/CrossUpdateController.js +16 -0
  24. package/dist/elements/src/elements/EFAudio.js +54 -0
  25. package/dist/elements/src/elements/EFCaptions.js +169 -0
  26. package/dist/elements/src/elements/EFImage.js +80 -0
  27. package/dist/elements/src/elements/EFMedia.js +356 -0
  28. package/dist/elements/src/elements/EFSourceMixin.js +55 -0
  29. package/dist/elements/src/elements/EFTemporal.js +283 -0
  30. package/dist/elements/src/elements/EFTimegroup.js +338 -0
  31. package/dist/elements/src/elements/EFVideo.js +110 -0
  32. package/dist/elements/src/elements/EFWaveform.js +226 -0
  33. package/dist/elements/src/elements/FetchMixin.js +28 -0
  34. package/dist/elements/src/elements/TimegroupController.js +20 -0
  35. package/dist/elements/src/elements/durationConverter.js +8 -0
  36. package/dist/elements/src/elements/parseTimeToMs.js +13 -0
  37. package/dist/elements/src/elements/util.js +11 -0
  38. package/dist/elements/src/gui/ContextMixin.js +246 -0
  39. package/dist/elements/src/gui/EFFilmstrip.js +731 -0
  40. package/dist/elements/src/gui/EFPreview.js +45 -0
  41. package/dist/elements/src/gui/EFToggleLoop.js +39 -0
  42. package/dist/elements/src/gui/EFTogglePlay.js +43 -0
  43. package/dist/elements/src/gui/EFWorkbench.js +128 -0
  44. package/dist/elements/src/gui/TWMixin.css.js +4 -0
  45. package/dist/elements/src/gui/TWMixin.js +36 -0
  46. package/dist/elements/src/gui/apiHostContext.js +5 -0
  47. package/dist/elements/src/gui/efContext.js +7 -0
  48. package/dist/elements/src/gui/fetchContext.js +5 -0
  49. package/dist/elements/src/gui/focusContext.js +5 -0
  50. package/dist/elements/src/gui/focusedElementContext.js +7 -0
  51. package/dist/elements/src/gui/playingContext.js +7 -0
  52. package/dist/elements/src/index.js +31 -0
  53. package/dist/elements/src/msToTimeCode.js +15 -0
  54. package/dist/elements/util.d.ts +3 -0
  55. package/dist/gui/ContextMixin.d.ts +19 -0
  56. package/dist/gui/EFFilmstrip.d.ts +148 -0
  57. package/dist/gui/EFPreview.d.ts +12 -0
  58. package/dist/gui/EFToggleLoop.d.ts +12 -0
  59. package/dist/gui/EFTogglePlay.d.ts +12 -0
  60. package/dist/gui/EFWorkbench.d.ts +18 -0
  61. package/dist/gui/TWMixin.d.ts +2 -0
  62. package/dist/gui/apiHostContext.d.ts +3 -0
  63. package/dist/gui/efContext.d.ts +4 -0
  64. package/dist/gui/fetchContext.d.ts +3 -0
  65. package/dist/gui/focusContext.d.ts +6 -0
  66. package/dist/gui/focusedElementContext.d.ts +3 -0
  67. package/dist/gui/playingContext.d.ts +6 -0
  68. package/dist/index.d.ts +12 -0
  69. package/dist/msToTimeCode.d.ts +1 -0
  70. package/dist/style.css +802 -0
  71. package/package.json +7 -10
  72. package/src/elements/EFAudio.ts +1 -1
  73. package/src/elements/EFCaptions.ts +23 -17
  74. package/src/elements/EFImage.ts +3 -3
  75. package/src/elements/EFMedia.ts +48 -17
  76. package/src/elements/EFSourceMixin.ts +1 -1
  77. package/src/elements/EFTemporal.ts +101 -6
  78. package/src/elements/EFTimegroup.browsertest.ts +3 -3
  79. package/src/elements/EFTimegroup.ts +30 -47
  80. package/src/elements/EFVideo.ts +2 -2
  81. package/src/elements/EFWaveform.ts +9 -9
  82. package/src/elements/FetchMixin.ts +5 -3
  83. package/src/elements/TimegroupController.ts +1 -1
  84. package/src/elements/durationConverter.ts +21 -1
  85. package/src/elements/parseTimeToMs.ts +1 -0
  86. package/src/elements/util.ts +1 -1
  87. package/src/gui/ContextMixin.ts +268 -0
  88. package/src/gui/EFFilmstrip.ts +61 -171
  89. package/src/gui/EFPreview.ts +39 -0
  90. package/src/gui/EFToggleLoop.ts +34 -0
  91. package/src/gui/EFTogglePlay.ts +38 -0
  92. package/src/gui/EFWorkbench.ts +11 -109
  93. package/src/gui/TWMixin.ts +10 -3
  94. package/src/gui/apiHostContext.ts +3 -0
  95. package/src/gui/efContext.ts +6 -0
  96. package/src/gui/fetchContext.ts +5 -0
  97. package/src/gui/focusContext.ts +7 -0
  98. package/src/gui/focusedElementContext.ts +5 -0
  99. package/src/gui/playingContext.ts +5 -0
  100. package/CHANGELOG.md +0 -7
  101. package/postcss.config.cjs +0 -12
  102. package/src/EF_INTERACTIVE.ts +0 -2
  103. package/src/elements.css +0 -22
  104. package/src/index.ts +0 -33
  105. package/tailwind.config.ts +0 -10
  106. package/tsconfig.json +0 -4
  107. package/vite.config.ts +0 -8
@@ -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)) {
@@ -351,7 +351,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
351
351
  if (!this.targetElement.audioBufferTask.value) {
352
352
  return;
353
353
  }
354
- if (this.targetElement.ownCurrentTimeMs > 0) {
354
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
355
355
  const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
356
356
  const audioBufferSource = audioContext.createBufferSource();
357
357
  audioBufferSource.buffer =
@@ -364,7 +364,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
364
364
  0,
365
365
  Math.max(
366
366
  0,
367
- (this.targetElement.ownCurrentTimeMs -
367
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs -
368
368
  this.targetElement.audioBufferTask.value.startOffsetMs) /
369
369
  1000,
370
370
  ),
@@ -412,6 +412,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
412
412
  if (target instanceof EFAudio || target instanceof EFVideo) {
413
413
  return target;
414
414
  }
415
- throw new Error("Invalid target, must be an EFAudio element");
415
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
416
416
  }
417
417
  }
@@ -1,8 +1,9 @@
1
- import { consume } from "@lit/context";
2
1
  import type { LitElement } from "lit";
3
- import { fetchContext } from "../gui/EFWorkbench";
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 = fetch.bind(window);
16
+ fetch: (url: string, init?: RequestInit) => Promise<Response> =
17
+ fetch.bind(window);
16
18
  }
17
19
 
18
20
  return FetchElement as Constructor<FetchMixinInterface> & T;
@@ -1,5 +1,5 @@
1
1
  import type { ReactiveController, LitElement } from "lit";
2
- import type { EFTimegroup } from "./EFTimegroup";
2
+ import type { EFTimegroup } from "./EFTimegroup.ts";
3
3
 
4
4
  export class TimegroupController implements ReactiveController {
5
5
  constructor(
@@ -1,6 +1,26 @@
1
- import { parseTimeToMs } from "./parseTimeToMs";
1
+ import { parseTimeToMs } from "./parseTimeToMs.ts";
2
2
 
3
3
  export const durationConverter = {
4
4
  fromAttribute: (value: string): number => parseTimeToMs(value),
5
5
  toAttribute: (value: number) => `${value}s`,
6
6
  };
7
+
8
+ const positiveDurationConverter = (error: string) => {
9
+ return {
10
+ fromAttribute: (value: string): number => {
11
+ if (value.startsWith("-")) {
12
+ throw new Error(error);
13
+ }
14
+ return parseTimeToMs(value);
15
+ },
16
+ toAttribute: (value: number) => `${value}s`,
17
+ };
18
+ };
19
+
20
+ export const trimDurationConverter = positiveDurationConverter(
21
+ "Trimstart & trimend must be a positive value in milliseconds or seconds (1s, 1000ms)",
22
+ );
23
+
24
+ export const imageDurationConverter = positiveDurationConverter(
25
+ "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
+ );
@@ -1,4 +1,5 @@
1
1
  export const parseTimeToMs = (time: string) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -1,4 +1,4 @@
1
- import { EFTimegroup } from "./EFTimegroup";
1
+ import { EFTimegroup } from "./EFTimegroup.ts";
2
2
 
3
3
  export const getRootTimeGroup = (element: Element): EFTimegroup | null => {
4
4
  let bestCandidate: EFTimegroup | null = null;
@@ -0,0 +1,268 @@
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 { createRef } from "lit/directives/ref.js";
9
+ import { loopContext, playingContext } from "./playingContext.ts";
10
+ import type { EFTimegroup } from "../elements/EFTimegroup.ts";
11
+ import { efContext } from "./efContext.ts";
12
+
13
+ export declare class ContextMixinInterface {
14
+ signingURL?: string;
15
+ rendering: boolean;
16
+ playing: boolean;
17
+ loop: boolean;
18
+ currentTimeMs: number;
19
+ focusedElement?: HTMLElement;
20
+ stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
21
+ canvasRef: ReturnType<typeof createRef<HTMLElement>>;
22
+ targetTimegroup: EFTimegroup | null;
23
+ play(): void;
24
+ pause(): void;
25
+ }
26
+
27
+ type Constructor<T = {}> = new (...args: any[]) => T;
28
+ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
29
+ class ContextElement extends superClass {
30
+ @provide({ context: focusContext })
31
+ focusContext = this as FocusContext;
32
+
33
+ @provide({ context: focusedElementContext })
34
+ @state()
35
+ focusedElement?: HTMLElement;
36
+
37
+ @provide({ context: efContext })
38
+ efContext = this;
39
+
40
+ @provide({ context: fetchContext })
41
+ fetch = async (url: string, init: RequestInit = {}) => {
42
+ init.headers ||= {};
43
+ Object.assign(init.headers, {
44
+ "Content-Type": "application/json",
45
+ });
46
+
47
+ if (this.signingURL) {
48
+ if (!this.#URLTokens[url]) {
49
+ this.#URLTokens[url] = fetch(this.signingURL, {
50
+ method: "POST",
51
+ body: JSON.stringify({ url }),
52
+ }).then(async (response) => {
53
+ if (response.ok) {
54
+ return (await response.json()).token;
55
+ }
56
+ throw new Error(
57
+ `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
58
+ );
59
+ });
60
+ }
61
+
62
+ const urlToken = await this.#URLTokens[url];
63
+
64
+ Object.assign(init.headers, {
65
+ authorization: `Bearer ${urlToken}`,
66
+ });
67
+ }
68
+
69
+ return fetch(url, init);
70
+ };
71
+
72
+ #URLTokens: Record<string, Promise<string>> = {};
73
+
74
+ /**
75
+ * A URL that will be used to generated signed tokens for accessing media files from the
76
+ * editframe API. This is used to authenticate media requests per-user.
77
+ */
78
+ @property({ type: String })
79
+ signingURL?: string;
80
+
81
+ @provide({ context: playingContext })
82
+ @property({ type: Boolean, reflect: true })
83
+ playing = false;
84
+
85
+ @provide({ context: loopContext })
86
+ @property({ type: Boolean, reflect: true })
87
+ loop = false;
88
+
89
+ @state()
90
+ private stageScale = 1;
91
+
92
+ @property({ type: Boolean })
93
+ rendering = false;
94
+
95
+ @state()
96
+ currentTimeMs = 0;
97
+
98
+ stageRef = createRef<HTMLDivElement>();
99
+ canvasRef = createRef<HTMLSlotElement>();
100
+
101
+ setStageScale = () => {
102
+ if (this.isConnected && !this.rendering) {
103
+ const canvasElement = this.canvasRef.value;
104
+ const stageElement = this.stageRef.value;
105
+ const canvasChild = canvasElement?.assignedElements()[0];
106
+ if (stageElement && canvasElement && canvasChild) {
107
+ // Determine the appropriate scale factor to make the canvas fit into
108
+ canvasElement.style.width = `${canvasChild.clientWidth}px`;
109
+ canvasElement.style.height = `${canvasChild.clientHeight}px`;
110
+ const stageWidth = stageElement.clientWidth;
111
+ const stageHeight = stageElement.clientHeight;
112
+ const canvasWidth = canvasElement.clientWidth;
113
+ const canvasHeight = canvasElement.clientHeight;
114
+ const stageRatio = stageWidth / stageHeight;
115
+ const canvasRatio = canvasWidth / canvasHeight;
116
+ if (stageRatio > canvasRatio) {
117
+ const scale = stageHeight / canvasHeight;
118
+ if (this.stageScale !== scale) {
119
+ canvasElement.style.transform = `scale(${scale})`;
120
+ canvasElement.style.transformOrigin = "top";
121
+ }
122
+ this.stageScale = scale;
123
+ } else {
124
+ const scale = stageWidth / canvasWidth;
125
+ if (this.stageScale !== scale) {
126
+ canvasElement.style.transform = `scale(${scale})`;
127
+ canvasElement.style.transformOrigin = "top";
128
+ }
129
+ this.stageScale = scale;
130
+ }
131
+ }
132
+ }
133
+ if (this.isConnected) {
134
+ requestAnimationFrame(this.setStageScale);
135
+ }
136
+ };
137
+
138
+ connectedCallback(): void {
139
+ super.connectedCallback();
140
+ // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
141
+ // timed correctly. So we use requestAnimationFrame as a stop-gap.
142
+ requestAnimationFrame(this.setStageScale);
143
+ }
144
+
145
+ update(changedProperties: Map<string | number | symbol, unknown>) {
146
+ if (changedProperties.has("playing")) {
147
+ if (this.playing) {
148
+ this.#startPlayback();
149
+ } else {
150
+ this.#stopPlayback();
151
+ }
152
+ }
153
+
154
+ if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
155
+ if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
156
+ this.targetTimegroup.currentTimeMs = this.currentTimeMs;
157
+ }
158
+ }
159
+ super.update(changedProperties);
160
+ }
161
+
162
+ get targetTimegroup() {
163
+ return this.querySelector("ef-timegroup");
164
+ }
165
+
166
+ play() {
167
+ this.playing = true;
168
+ }
169
+
170
+ pause() {
171
+ this.playing = false;
172
+ }
173
+
174
+ #playbackAudioContext: AudioContext | null = null;
175
+ #playbackAnimationFrameRequest: number | null = null;
176
+ #AUDIO_PLAYBACK_SLICE_MS = 1000;
177
+
178
+ #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
179
+ this.currentTimeMs =
180
+ startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
181
+ this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
182
+ this.#syncPlayheadToAudioContext(target, startMs);
183
+ });
184
+ }
185
+
186
+ async #stopPlayback() {
187
+ if (this.#playbackAudioContext) {
188
+ if (this.#playbackAudioContext.state !== "closed") {
189
+ await this.#playbackAudioContext.close();
190
+ }
191
+ }
192
+ if (this.#playbackAnimationFrameRequest) {
193
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
194
+ }
195
+ this.#playbackAudioContext = null;
196
+ }
197
+
198
+ async #startPlayback() {
199
+ await this.#stopPlayback();
200
+ const timegroup = this.targetTimegroup;
201
+ if (!timegroup) {
202
+ return;
203
+ }
204
+
205
+ let currentMs = timegroup.currentTimeMs;
206
+ let bufferCount = 0;
207
+ this.#playbackAudioContext = new AudioContext({
208
+ latencyHint: "playback",
209
+ });
210
+ if (this.#playbackAnimationFrameRequest) {
211
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
212
+ }
213
+ this.#syncPlayheadToAudioContext(timegroup, currentMs);
214
+ const playbackContext = this.#playbackAudioContext;
215
+ await playbackContext.suspend();
216
+
217
+ const fillBuffer = async () => {
218
+ if (bufferCount > 1) {
219
+ return;
220
+ }
221
+ const canFillBuffer = await queueBufferSource();
222
+ if (canFillBuffer) {
223
+ fillBuffer();
224
+ }
225
+ };
226
+
227
+ const fromMs = currentMs;
228
+ const toMs = timegroup.endTimeMs;
229
+
230
+ const queueBufferSource = async () => {
231
+ if (currentMs >= toMs) {
232
+ return false;
233
+ }
234
+ const startMs = currentMs;
235
+ const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
236
+ currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
237
+ const audioBuffer = await timegroup.renderAudio(startMs, endMs);
238
+ bufferCount++;
239
+ const source = playbackContext.createBufferSource();
240
+ source.buffer = audioBuffer;
241
+ source.connect(playbackContext.destination);
242
+ source.start((startMs - fromMs) / 1000);
243
+ source.onended = () => {
244
+ bufferCount--;
245
+ if (endMs >= toMs) {
246
+ this.pause();
247
+ if (this.loop) {
248
+ this.updateComplete.then(() => {
249
+ this.currentTimeMs = 0;
250
+ this.updateComplete.then(() => {
251
+ this.play();
252
+ });
253
+ });
254
+ }
255
+ } else {
256
+ fillBuffer();
257
+ }
258
+ };
259
+ return true;
260
+ };
261
+
262
+ await fillBuffer();
263
+ await playbackContext.resume();
264
+ }
265
+ }
266
+
267
+ return ContextElement as Constructor<ContextMixinInterface> & T;
268
+ }