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

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 (97) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +44 -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 +9 -0
  7. package/dist/elements/EFAudio.d.ts +10 -0
  8. package/dist/elements/EFCaptions.d.ts +38 -0
  9. package/dist/elements/EFImage.d.ts +14 -0
  10. package/dist/elements/EFMedia.d.ts +61 -0
  11. package/dist/elements/EFSourceMixin.d.ts +12 -0
  12. package/dist/elements/EFTemporal.d.ts +38 -0
  13. package/dist/elements/EFTimegroup.browsertest.d.ts +12 -0
  14. package/dist/elements/EFTimegroup.d.ts +39 -0
  15. package/dist/elements/EFVideo.d.ts +14 -0
  16. package/dist/elements/EFWaveform.d.ts +30 -0
  17. package/dist/elements/FetchMixin.d.ts +8 -0
  18. package/dist/elements/TimegroupController.d.ts +14 -0
  19. package/dist/elements/durationConverter.d.ts +4 -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 +166 -0
  26. package/dist/elements/src/elements/EFImage.js +80 -0
  27. package/dist/elements/src/elements/EFMedia.js +339 -0
  28. package/dist/elements/src/elements/EFSourceMixin.js +55 -0
  29. package/dist/elements/src/elements/EFTemporal.js +234 -0
  30. package/dist/elements/src/elements/EFTimegroup.js +355 -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 +12 -0
  37. package/dist/elements/src/elements/util.js +11 -0
  38. package/dist/elements/src/gui/ContextMixin.js +234 -0
  39. package/dist/elements/src/gui/EFFilmstrip.js +729 -0
  40. package/dist/elements/src/gui/EFPreview.js +45 -0
  41. package/dist/elements/src/gui/EFWorkbench.js +128 -0
  42. package/dist/elements/src/gui/TWMixin.css.js +4 -0
  43. package/dist/elements/src/gui/TWMixin.js +36 -0
  44. package/dist/elements/src/gui/apiHostContext.js +5 -0
  45. package/dist/elements/src/gui/fetchContext.js +5 -0
  46. package/dist/elements/src/gui/focusContext.js +5 -0
  47. package/dist/elements/src/gui/focusedElementContext.js +7 -0
  48. package/dist/elements/src/gui/playingContext.js +5 -0
  49. package/dist/elements/src/index.js +27 -0
  50. package/dist/elements/src/msToTimeCode.js +15 -0
  51. package/dist/elements/util.d.ts +4 -0
  52. package/dist/gui/ContextMixin.d.ts +23 -0
  53. package/dist/gui/EFFilmstrip.d.ts +144 -0
  54. package/dist/gui/EFPreview.d.ts +27 -0
  55. package/dist/gui/EFWorkbench.d.ts +34 -0
  56. package/dist/gui/TWMixin.d.ts +3 -0
  57. package/dist/gui/apiHostContext.d.ts +3 -0
  58. package/dist/gui/fetchContext.d.ts +3 -0
  59. package/dist/gui/focusContext.d.ts +6 -0
  60. package/dist/gui/focusedElementContext.d.ts +3 -0
  61. package/dist/gui/playingContext.d.ts +3 -0
  62. package/dist/index.d.ts +11 -0
  63. package/dist/msToTimeCode.d.ts +1 -0
  64. package/dist/style.css +800 -0
  65. package/package.json +6 -9
  66. package/src/elements/EFAudio.ts +1 -1
  67. package/src/elements/EFCaptions.ts +9 -9
  68. package/src/elements/EFImage.ts +3 -3
  69. package/src/elements/EFMedia.ts +11 -8
  70. package/src/elements/EFSourceMixin.ts +1 -1
  71. package/src/elements/EFTemporal.ts +42 -5
  72. package/src/elements/EFTimegroup.browsertest.ts +3 -3
  73. package/src/elements/EFTimegroup.ts +9 -6
  74. package/src/elements/EFVideo.ts +2 -2
  75. package/src/elements/EFWaveform.ts +6 -6
  76. package/src/elements/FetchMixin.ts +5 -3
  77. package/src/elements/TimegroupController.ts +1 -1
  78. package/src/elements/durationConverter.ts +1 -1
  79. package/src/elements/util.ts +1 -1
  80. package/src/gui/ContextMixin.ts +254 -0
  81. package/src/gui/EFFilmstrip.ts +41 -150
  82. package/src/gui/EFPreview.ts +39 -0
  83. package/src/gui/EFWorkbench.ts +7 -105
  84. package/src/gui/TWMixin.ts +10 -3
  85. package/src/gui/apiHostContext.ts +3 -0
  86. package/src/gui/fetchContext.ts +5 -0
  87. package/src/gui/focusContext.ts +7 -0
  88. package/src/gui/focusedElementContext.ts +5 -0
  89. package/src/gui/playingContext.ts +3 -0
  90. package/CHANGELOG.md +0 -7
  91. package/postcss.config.cjs +0 -12
  92. package/src/EF_INTERACTIVE.ts +0 -2
  93. package/src/elements.css +0 -22
  94. package/src/index.ts +0 -33
  95. package/tailwind.config.ts +0 -10
  96. package/tsconfig.json +0 -4
  97. package/vite.config.ts +0 -8
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.7.0-beta.9",
3
+ "version": "0.8.0-beta.1",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": {
8
- "default": "./dist/packages/elements/src/index.js",
9
- "types": "./dist/packages/elements/src/index.d.ts"
10
- },
11
- "require": {
12
- "default": "./dist/packages/elements/src/index.cjs",
13
- "types": "./dist/packages/elements/src/index.d.ts"
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/elements/src/index.js"
14
10
  }
15
11
  },
16
12
  "./styles.css": "./dist/style.css"
@@ -24,7 +20,7 @@
24
20
  "author": "",
25
21
  "license": "UNLICENSED",
26
22
  "dependencies": {
27
- "@editframe/assets": "0.7.0-beta.8",
23
+ "@editframe/assets": "0.8.0-beta.1",
28
24
  "@lit/context": "^1.1.2",
29
25
  "@lit/task": "^1.0.1",
30
26
  "d3": "^7.9.0",
@@ -35,9 +31,10 @@
35
31
  "devDependencies": {
36
32
  "@types/d3": "^7.4.3",
37
33
  "@types/dom-webcodecs": "^0.1.11",
38
- "@types/node": "^20.14.9",
34
+ "@types/node": "^20.14.13",
39
35
  "autoprefixer": "^10.4.19",
40
36
  "rollup-plugin-tsconfig-paths": "^1.5.2",
37
+ "typescript": "^5.5.4",
41
38
  "vite-plugin-dts": "^3.9.1",
42
39
  "vite-tsconfig-paths": "^4.3.2"
43
40
  }
@@ -1,7 +1,7 @@
1
1
  import { html } from "lit";
2
2
  import { createRef, ref } from "lit/directives/ref.js";
3
3
  import { customElement, property } from "lit/decorators.js";
4
- import { EFMedia } from "./EFMedia";
4
+ import { EFMedia } from "./EFMedia.ts";
5
5
  import { Task } from "@lit/task";
6
6
 
7
7
  @customElement("ef-audio")
@@ -1,13 +1,13 @@
1
- import { EFAudio } from "./EFAudio";
2
1
  import { LitElement, type PropertyValueMap, html, css } from "lit";
3
2
  import { Task } from "@lit/task";
4
3
  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";
4
+ import { EFVideo } from "./EFVideo.ts";
5
+ import { EFAudio } from "./EFAudio.ts";
6
+ import { EFTemporal } from "./EFTemporal.ts";
7
+ import { CrossUpdateController } from "./CrossUpdateController.ts";
8
+ import { FetchMixin } from "./FetchMixin.ts";
9
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
10
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
11
11
 
12
12
  interface Word {
13
13
  text: string;
@@ -93,8 +93,8 @@ export class EFCaptions extends EFSourceMixin(
93
93
 
94
94
  protected md5SumLoader = new Task(this, {
95
95
  autoRun: false,
96
- args: () => [this.target] as const,
97
- task: async ([], { signal }) => {
96
+ args: () => [this.target, this.fetch] as const,
97
+ task: async ([_target, fetch], { signal }) => {
98
98
  const md5Path = `/@ef-asset/${this.targetElement.src ?? ""}`;
99
99
  const response = await fetch(md5Path, { method: "HEAD", signal });
100
100
  return response.headers.get("etag") ?? undefined;
@@ -2,9 +2,9 @@ import { Task } from "@lit/task";
2
2
  import { LitElement, html, css } from "lit";
3
3
  import { customElement } from "lit/decorators.js";
4
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";
5
+ import { FetchMixin } from "./FetchMixin.ts";
6
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
7
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
8
8
 
9
9
  @customElement("ef-image")
10
10
  export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
@@ -8,14 +8,14 @@ import debug from "debug";
8
8
 
9
9
  import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
10
10
 
11
- import { MP4File } from "@/av/MP4File";
12
- import { EFTemporal } from "./EFTemporal";
13
- import { VideoAsset } from "@/av/EncodedAsset";
14
- import { FetchMixin } from "./FetchMixin";
15
- import { apiHostContext } from "../gui/EFWorkbench";
16
- import { EFSourceMixin } from "./EFSourceMixin";
17
- import { getStartTimeMs } from "./util";
18
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
11
+ import { MP4File } from "@editframe/assets/MP4File.js";
12
+ import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
13
+ import { EFTemporal } from "./EFTemporal.ts";
14
+ import { FetchMixin } from "./FetchMixin.ts";
15
+ import { EFSourceMixin } from "./EFSourceMixin.ts";
16
+ import { getStartTimeMs } from "./util.ts";
17
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
18
+ import { apiHostContext } from "../gui/apiHostContext.ts";
19
19
 
20
20
  const log = debug("ef:elements:EFMedia");
21
21
 
@@ -70,6 +70,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
70
70
  public trackFragmentIndexLoader = new Task(this, {
71
71
  args: () => [this.fragmentIndexPath(), this.fetch] as const,
72
72
  task: async ([fragmentIndexPath, fetch], { signal }) => {
73
+ if (this.src === "") {
74
+ return;
75
+ }
73
76
  const response = await fetch(fragmentIndexPath, { signal });
74
77
  return (await response.json()) as Record<number, TrackFragmentIndex>;
75
78
  },
@@ -1,9 +1,9 @@
1
1
  import { consume } from "@lit/context";
2
2
  import type { LitElement } from "lit";
3
- import { apiHostContext } from "../gui/EFWorkbench";
4
3
  import { state } from "lit/decorators/state.js";
5
4
  import { Task } from "@lit/task";
6
5
  import { property } from "lit/decorators/property.js";
6
+ import { apiHostContext } from "../gui/apiHostContext.ts";
7
7
 
8
8
  export declare class EFSourceMixinInterface {
9
9
  productionSrc(): string;
@@ -1,11 +1,11 @@
1
1
  import type { LitElement, ReactiveController } from "lit";
2
2
  import { consume, createContext } from "@lit/context";
3
3
  import { property, state } from "lit/decorators.js";
4
- import type { EFTimegroup } from "./EFTimegroup";
4
+ import type { EFTimegroup } from "./EFTimegroup.ts";
5
5
 
6
- import { durationConverter } from "./durationConverter";
6
+ import { durationConverter } from "./durationConverter.ts";
7
7
  import { Task } from "@lit/task";
8
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
8
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
9
9
 
10
10
  export const timegroupContext = createContext<EFTimegroup>(
11
11
  Symbol("timeGroupContext"),
@@ -59,10 +59,23 @@ export const deepGetElementsWithFrameTasks = (
59
59
  return elements;
60
60
  };
61
61
 
62
+ let temporalCache: Map<Element, TemporalMixinInterface[]>;
63
+ const resetTemporalCache = () => {
64
+ temporalCache = new Map();
65
+ if (typeof requestAnimationFrame !== "undefined") {
66
+ requestAnimationFrame(resetTemporalCache);
67
+ }
68
+ };
69
+ resetTemporalCache();
70
+
62
71
  export const shallowGetTemporalElements = (
63
72
  element: Element,
64
73
  temporals: TemporalMixinInterface[] = [],
65
74
  ) => {
75
+ const cachedResult = temporalCache.get(element);
76
+ if (cachedResult) {
77
+ return cachedResult;
78
+ }
66
79
  for (const child of element.children) {
67
80
  if (isEFTemporal(child)) {
68
81
  temporals.push(child);
@@ -70,6 +83,7 @@ export const shallowGetTemporalElements = (
70
83
  shallowGetTemporalElements(child, temporals);
71
84
  }
72
85
  }
86
+ temporalCache.set(element, temporals);
73
87
  return temporals;
74
88
  };
75
89
 
@@ -92,6 +106,15 @@ export class OwnCurrentTimeController implements ReactiveController {
92
106
 
93
107
  type Constructor<T = {}> = new (...args: any[]) => T;
94
108
 
109
+ let startTimeMsCache = new WeakMap<Element, number>();
110
+ const resetStartTimeMsCache = () => {
111
+ startTimeMsCache = new WeakMap();
112
+ if (typeof requestAnimationFrame !== "undefined") {
113
+ requestAnimationFrame(resetStartTimeMsCache);
114
+ }
115
+ };
116
+ resetStartTimeMsCache();
117
+
95
118
  export const EFTemporal = <T extends Constructor<LitElement>>(
96
119
  superClass: T,
97
120
  ) => {
@@ -172,27 +195,41 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
172
195
  }
173
196
 
174
197
  get startTimeMs(): number {
198
+ const cachedStartTime = startTimeMsCache.get(this);
199
+ if (cachedStartTime !== undefined) {
200
+ return cachedStartTime;
201
+ }
175
202
  const parentTimegroup = this.parentTimegroup;
176
203
  if (!parentTimegroup) {
204
+ startTimeMsCache.set(this, 0);
177
205
  return 0;
178
206
  }
179
207
  switch (parentTimegroup.mode) {
180
208
  case "sequence": {
181
209
  const siblingTemorals = shallowGetTemporalElements(parentTimegroup);
182
- const ownIndex = siblingTemorals.indexOf(
210
+ const ownIndex = siblingTemorals?.indexOf(
183
211
  this as InstanceType<Constructor<TemporalMixinInterface> & T>,
184
212
  );
185
213
  if (ownIndex === 0) {
214
+ startTimeMsCache.set(this, parentTimegroup.startTimeMs);
186
215
  return parentTimegroup.startTimeMs;
187
216
  }
188
- const previous = siblingTemorals[ownIndex - 1];
217
+ const previous = siblingTemorals?.[(ownIndex ?? 0) - 1];
189
218
  if (!previous) {
190
219
  throw new Error("Previous temporal element not found");
191
220
  }
221
+ startTimeMsCache.set(
222
+ this,
223
+ previous.startTimeMs + previous.durationMs,
224
+ );
192
225
  return previous.startTimeMs + previous.durationMs;
193
226
  }
194
227
  case "contain":
195
228
  case "fixed":
229
+ startTimeMsCache.set(
230
+ this,
231
+ parentTimegroup.startTimeMs + this.offsetMs,
232
+ );
196
233
  return parentTimegroup.startTimeMs + this.offsetMs;
197
234
  default:
198
235
  throw new Error(`Invalid time mode: ${parentTimegroup.mode}`);
@@ -5,10 +5,10 @@ import {
5
5
  html,
6
6
  render as litRender,
7
7
  } from "lit";
8
- import { EFTimegroup } from "./EFTimegroup";
9
- import "./EFTimegroup";
8
+ import { EFTimegroup } from "./EFTimegroup.ts";
9
+ import "./EFTimegroup.ts";
10
10
  import { customElement } from "lit/decorators/custom-element.js";
11
- import { EFTemporal } from "./EFTemporal";
11
+ import { EFTemporal } from "./EFTemporal.ts";
12
12
 
13
13
  beforeEach(() => {
14
14
  for (let i = 0; i < localStorage.length; i++) {
@@ -9,10 +9,10 @@ import {
9
9
  isEFTemporal,
10
10
  shallowGetTemporalElements,
11
11
  timegroupContext,
12
- } from "./EFTemporal";
13
- import { TimegroupController } from "./TimegroupController";
14
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE";
15
- import { deepGetMediaElements } from "./EFMedia";
12
+ } from "./EFTemporal.ts";
13
+ import { TimegroupController } from "./TimegroupController.ts";
14
+ import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
+ import { deepGetMediaElements } from "./EFMedia.ts";
16
16
 
17
17
  const log = debug("ef:elements:EFTimegroup");
18
18
 
@@ -113,7 +113,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
113
113
  connectedCallback() {
114
114
  super.connectedCallback();
115
115
  if (this.id) {
116
- this.#currentTime = this.maybeLoadTimeFromLocalStorage();
116
+ this.waitForMediaDurations().then(() => {
117
+ this.#currentTime = this.maybeLoadTimeFromLocalStorage();
118
+ });
117
119
  }
118
120
 
119
121
  if (this.parentTimegroup) {
@@ -265,7 +267,8 @@ export class EFTimegroup extends EFTemporal(LitElement) {
265
267
  return (
266
268
  EF_INTERACTIVE &&
267
269
  this.closest("ef-timegroup") === this &&
268
- this.closest("ef-workbench") === null
270
+ this.closest("ef-workbench") === null &&
271
+ this.closest("ef-preview") === null
269
272
  );
270
273
  }
271
274
 
@@ -3,8 +3,8 @@ import { Task } from "@lit/task";
3
3
  import { createRef, ref } from "lit/directives/ref.js";
4
4
  import { customElement } from "lit/decorators.js";
5
5
 
6
- import { EFMedia } from "./EFMedia";
7
- import { TWMixin } from "../gui/TWMixin";
6
+ import { EFMedia } from "./EFMedia.ts";
7
+ import { TWMixin } from "../gui/TWMixin.ts";
8
8
 
9
9
  @customElement("ef-video")
10
10
  export class EFVideo extends TWMixin(EFMedia) {
@@ -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)) {
@@ -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,4 +1,4 @@
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),
@@ -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,254 @@
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.#signedURLs[url]) {
55
+ this.#signedURLs[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()).url;
61
+ }
62
+ throw new Error(
63
+ `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
64
+ );
65
+ });
66
+ }
67
+
68
+ const signedURL = await this.#signedURLs[url];
69
+
70
+ return fetch(signedURL, init);
71
+ }
72
+
73
+ return fetch(url, init);
74
+ };
75
+
76
+ #signedURLs: Record<string, Promise<string>> = {};
77
+
78
+ @property({ type: String })
79
+ signingURL?: string;
80
+
81
+ @property({ type: String })
82
+ apiToken?: string;
83
+
84
+ @provide({ context: apiHostContext })
85
+ @property({ type: String })
86
+ apiHost = "";
87
+
88
+ @provide({ context: playingContext })
89
+ @property({ type: Boolean, reflect: true })
90
+ playing = false;
91
+
92
+ @property({ type: Boolean, reflect: true })
93
+ loop = false;
94
+
95
+ @state()
96
+ stageScale = 1;
97
+
98
+ @property({ type: Boolean })
99
+ rendering = false;
100
+
101
+ @state()
102
+ currentTimeMs = 0;
103
+
104
+ stageRef = createRef<HTMLDivElement>();
105
+ canvasRef = createRef<HTMLSlotElement>();
106
+
107
+ setStageScale = () => {
108
+ if (this.isConnected && !this.rendering) {
109
+ const canvasElement = this.canvasRef.value;
110
+ const stageElement = this.stageRef.value;
111
+ if (stageElement && canvasElement) {
112
+ // Determine the appropriate scale factor to make the canvas fit into
113
+ // it's parent element.
114
+ const stageWidth = stageElement.clientWidth;
115
+ const stageHeight = stageElement.clientHeight;
116
+ const canvasWidth = canvasElement.clientWidth;
117
+ const canvasHeight = canvasElement.clientHeight;
118
+ const stageRatio = stageWidth / stageHeight;
119
+ const canvasRatio = canvasWidth / canvasHeight;
120
+ if (stageRatio > canvasRatio) {
121
+ const scale = stageHeight / canvasHeight;
122
+ if (this.stageScale !== scale) {
123
+ canvasElement.style.transform = `scale(${scale})`;
124
+ }
125
+ this.stageScale = scale;
126
+ } else {
127
+ const scale = stageWidth / canvasWidth;
128
+ if (this.stageScale !== scale) {
129
+ canvasElement.style.transform = `scale(${scale})`;
130
+ }
131
+ this.stageScale = scale;
132
+ }
133
+ }
134
+ }
135
+ if (this.isConnected) {
136
+ requestAnimationFrame(this.setStageScale);
137
+ }
138
+ };
139
+
140
+ connectedCallback(): void {
141
+ super.connectedCallback();
142
+ // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
143
+ // timed correctly. So we use requestAnimationFrame as a stop-gap.
144
+ requestAnimationFrame(this.setStageScale);
145
+ }
146
+
147
+ update(changedProperties: Map<string | number | symbol, unknown>) {
148
+ if (changedProperties.has("playing")) {
149
+ if (this.playing) {
150
+ this.#startPlayback();
151
+ } else {
152
+ this.#stopPlayback();
153
+ }
154
+ }
155
+
156
+ if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
157
+ if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
158
+ this.targetTimegroup.currentTimeMs = this.currentTimeMs;
159
+ }
160
+ }
161
+ super.update(changedProperties);
162
+ }
163
+
164
+ get targetTimegroup() {
165
+ return this.querySelector("ef-timegroup");
166
+ }
167
+
168
+ #playbackAudioContext: AudioContext | null = null;
169
+ #playbackAnimationFrameRequest: number | null = null;
170
+ #AUDIO_PLAYBACK_SLICE_MS = 1000;
171
+
172
+ #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
173
+ this.currentTimeMs =
174
+ startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
175
+ this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
176
+ this.#syncPlayheadToAudioContext(target, startMs);
177
+ });
178
+ }
179
+
180
+ async #stopPlayback() {
181
+ if (this.#playbackAudioContext) {
182
+ if (this.#playbackAudioContext.state !== "closed") {
183
+ await this.#playbackAudioContext.close();
184
+ }
185
+ }
186
+ if (this.#playbackAnimationFrameRequest) {
187
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
188
+ }
189
+ this.#playbackAudioContext = null;
190
+ }
191
+
192
+ async #startPlayback() {
193
+ await this.#stopPlayback();
194
+ const timegroup = this.targetTimegroup;
195
+ if (!timegroup) {
196
+ return;
197
+ }
198
+
199
+ let currentMs = timegroup.currentTimeMs;
200
+ let bufferCount = 0;
201
+ this.#playbackAudioContext = new AudioContext({
202
+ latencyHint: "playback",
203
+ });
204
+ if (this.#playbackAnimationFrameRequest) {
205
+ cancelAnimationFrame(this.#playbackAnimationFrameRequest);
206
+ }
207
+ this.#syncPlayheadToAudioContext(timegroup, currentMs);
208
+ const playbackContext = this.#playbackAudioContext;
209
+ await playbackContext.suspend();
210
+
211
+ const fillBuffer = async () => {
212
+ if (bufferCount > 1) {
213
+ return;
214
+ }
215
+ const canFillBuffer = await queueBufferSource();
216
+ if (canFillBuffer) {
217
+ fillBuffer();
218
+ }
219
+ };
220
+
221
+ const fromMs = currentMs;
222
+ const toMs = timegroup.endTimeMs;
223
+
224
+ const queueBufferSource = async () => {
225
+ if (currentMs >= toMs) {
226
+ return false;
227
+ }
228
+ const startMs = currentMs;
229
+ const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
230
+ currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
231
+ const audioBuffer = await timegroup.renderAudio(startMs, endMs);
232
+ bufferCount++;
233
+ const source = playbackContext.createBufferSource();
234
+ source.buffer = audioBuffer;
235
+ source.connect(playbackContext.destination);
236
+ source.start((startMs - fromMs) / 1000);
237
+ source.onended = () => {
238
+ bufferCount--;
239
+ if (endMs >= toMs) {
240
+ this.playing = false;
241
+ } else {
242
+ fillBuffer();
243
+ }
244
+ };
245
+ return true;
246
+ };
247
+
248
+ await fillBuffer();
249
+ await playbackContext.resume();
250
+ }
251
+ }
252
+
253
+ return ContextElement as Constructor<ContextMixinInterface> & T;
254
+ }