@editframe/elements 0.10.0-beta.3 → 0.10.0-beta.4

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.
@@ -1,5 +1,6 @@
1
1
  import { LitElement } from 'lit';
2
2
  export declare class EFSourceMixinInterface {
3
+ apiHost?: string;
3
4
  productionSrc(): string;
4
5
  src: string;
5
6
  }
@@ -97,7 +97,7 @@ let EFCaptions = class extends EFSourceMixin(
97
97
  if (EF_RENDERING()) {
98
98
  return `editframe://api/v1/caption_files/${this.targetElement.assetId}`;
99
99
  }
100
- return `https://editframe.dev/api/v1/caption_files/${this.targetElement.assetId}`;
100
+ return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
101
101
  }
102
102
  const targetSrc = this.targetElement.src;
103
103
  return `/@ef-captions/${targetSrc}`;
@@ -78,7 +78,7 @@ let EFImage = class extends EFSourceMixin(FetchMixin(LitElement), {
78
78
  if (EF_RENDERING()) {
79
79
  return `editframe://api/v1/image_files/${this.assetId}`;
80
80
  }
81
- return `https://editframe.dev/api/v1/image_files/${this.assetId}`;
81
+ return `${this.apiHost}/api/v1/image_files/${this.assetId}`;
82
82
  }
83
83
  return `/@ef-image/${this.src}`;
84
84
  }
@@ -245,7 +245,7 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
245
245
  if (EF_RENDERING()) {
246
246
  return `editframe://api/v1/isobmff_files/${this.assetId}/index`;
247
247
  }
248
- return `https://editframe.dev/api/v1/isobmff_files/${this.assetId}/index`;
248
+ return `${this.apiHost}/api/v1/isobmff_files/${this.assetId}/index`;
249
249
  }
250
250
  return `/@ef-track-fragment-index/${this.src ?? ""}`;
251
251
  }
@@ -254,7 +254,7 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
254
254
  if (EF_RENDERING()) {
255
255
  return `editframe://api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
256
256
  }
257
- return `https://editframe.dev/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
257
+ return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
258
258
  }
259
259
  return `/@ef-track/${this.src ?? ""}?trackId=${trackId}`;
260
260
  }
@@ -27,24 +27,27 @@ function EFSourceMixin(superClass, options) {
27
27
  }
28
28
  });
29
29
  }
30
+ get apiHost() {
31
+ return this._apiHost ?? "https://editframe.dev";
32
+ }
30
33
  productionSrc() {
31
34
  if (!this.md5SumLoader.value) {
32
35
  throw new Error(
33
36
  `MD5 sum not available for ${this}. Cannot generate production URL`
34
37
  );
35
38
  }
36
- if (!this.efHost) {
39
+ if (!this.apiHost) {
37
40
  throw new Error(
38
- `efHost not available for ${this}. Cannot generate production URL`
41
+ `apiHost not available for ${this}. Cannot generate production URL`
39
42
  );
40
43
  }
41
- return `${this.efHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;
44
+ return `${this.apiHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;
42
45
  }
43
46
  }
44
47
  __decorateClass([
45
48
  consume({ context: apiHostContext, subscribe: true }),
46
49
  state()
47
- ], EFSourceElement.prototype, "efHost");
50
+ ], EFSourceElement.prototype, "_apiHost");
48
51
  __decorateClass([
49
52
  property({ type: String })
50
53
  ], EFSourceElement.prototype, "src");
@@ -6,6 +6,7 @@ import { fetchContext } from "./fetchContext.js";
6
6
  import { createRef } from "lit/directives/ref.js";
7
7
  import { playingContext, loopContext } from "./playingContext.js";
8
8
  import { efContext } from "./efContext.js";
9
+ import { apiHostContext } from "./apiHostContext.js";
9
10
  var __defProp = Object.defineProperty;
10
11
  var __decorateClass = (decorators, target, key, kind) => {
11
12
  var result = void 0;
@@ -98,13 +99,20 @@ function ContextMixin(superClass) {
98
99
  connectedCallback() {
99
100
  super.connectedCallback();
100
101
  requestAnimationFrame(this.setStageScale);
102
+ if (this.playing) {
103
+ this.startPlayback();
104
+ }
105
+ }
106
+ disconnectedCallback() {
107
+ super.disconnectedCallback();
108
+ this.stopPlayback();
101
109
  }
102
110
  update(changedProperties) {
103
111
  if (changedProperties.has("playing")) {
104
112
  if (this.playing) {
105
- this.#startPlayback();
113
+ this.startPlayback();
106
114
  } else {
107
- this.#stopPlayback();
115
+ this.stopPlayback();
108
116
  }
109
117
  }
110
118
  if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
@@ -132,7 +140,7 @@ function ContextMixin(superClass) {
132
140
  this.#syncPlayheadToAudioContext(target, startMs);
133
141
  });
134
142
  }
135
- async #stopPlayback() {
143
+ async stopPlayback() {
136
144
  if (this.#playbackAudioContext) {
137
145
  if (this.#playbackAudioContext.state !== "closed") {
138
146
  await this.#playbackAudioContext.close();
@@ -143,12 +151,13 @@ function ContextMixin(superClass) {
143
151
  }
144
152
  this.#playbackAudioContext = null;
145
153
  }
146
- async #startPlayback() {
147
- await this.#stopPlayback();
154
+ async startPlayback() {
155
+ await this.stopPlayback();
148
156
  const timegroup = this.targetTimegroup;
149
157
  if (!timegroup) {
150
158
  return;
151
159
  }
160
+ await timegroup.waitForMediaDurations();
152
161
  let currentMs = timegroup.currentTimeMs;
153
162
  let bufferCount = 0;
154
163
  this.#playbackAudioContext = new AudioContext({
@@ -159,6 +168,13 @@ function ContextMixin(superClass) {
159
168
  }
160
169
  this.#syncPlayheadToAudioContext(timegroup, currentMs);
161
170
  const playbackContext = this.#playbackAudioContext;
171
+ if (playbackContext.state === "suspended") {
172
+ console.warn(
173
+ "AudioContext is suspended, media playback will not work until user has interacted with page."
174
+ );
175
+ this.playing = false;
176
+ return;
177
+ }
162
178
  await playbackContext.suspend();
163
179
  const fillBuffer = async () => {
164
180
  if (bufferCount > 1) {
@@ -213,6 +229,10 @@ function ContextMixin(superClass) {
213
229
  provide({ context: focusedElementContext }),
214
230
  state()
215
231
  ], ContextElement.prototype, "focusedElement");
232
+ __decorateClass([
233
+ provide({ context: apiHostContext }),
234
+ property({ type: String, reflect: true, attribute: "api-host" })
235
+ ], ContextElement.prototype, "apiHost");
216
236
  __decorateClass([
217
237
  provide({ context: efContext })
218
238
  ], ContextElement.prototype, "efContext");
@@ -1,5 +1,7 @@
1
1
  import { createContext } from "@lit/context";
2
- const apiHostContext = createContext(Symbol("apiHostContext"));
2
+ const apiHostContext = createContext(
3
+ Symbol("apiHostContext")
4
+ );
3
5
  export {
4
6
  apiHostContext
5
7
  };
@@ -0,0 +1,18 @@
1
+ import { LitElement } from 'lit';
2
+ declare const TestContext_base: (new (...args: any[]) => import('./ContextMixin.ts').ContextMixinInterface) & typeof LitElement;
3
+ declare class TestContext extends TestContext_base {
4
+ }
5
+ declare global {
6
+ interface HTMLElementTagNameMap {
7
+ "test-context": TestContext;
8
+ }
9
+ }
10
+ declare class EFHostConsumer extends LitElement {
11
+ apiHost?: string;
12
+ }
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ "ef-host-consumer": EFHostConsumer;
16
+ }
17
+ }
18
+ export {};
@@ -3,6 +3,7 @@ import { createRef } from 'lit/directives/ref.js';
3
3
  import { EFTimegroup } from '../elements/EFTimegroup.ts';
4
4
  export declare class ContextMixinInterface {
5
5
  signingURL?: string;
6
+ apiHost?: string;
6
7
  rendering: boolean;
7
8
  playing: boolean;
8
9
  loop: boolean;
@@ -1,3 +1,3 @@
1
1
  export declare const apiHostContext: {
2
- __context__: string;
2
+ __context__: string | undefined;
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.10.0-beta.3",
3
+ "version": "0.10.0-beta.4",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -20,7 +20,7 @@
20
20
  "author": "",
21
21
  "license": "UNLICENSED",
22
22
  "dependencies": {
23
- "@editframe/assets": "0.10.0-beta.3",
23
+ "@editframe/assets": "0.10.0-beta.4",
24
24
  "@lit/context": "^1.1.2",
25
25
  "@lit/task": "^1.0.1",
26
26
  "d3": "^7.9.0",
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import "../gui/EFPreview.ts";
2
3
  import "./EFCaptions.ts";
3
4
  import "./EFVideo.ts";
4
5
  import { v4 } from "uuid";
@@ -46,5 +47,23 @@ describe("EFCaptions", () => {
46
47
  `https://editframe.dev/api/v1/caption_files/${id}:example.mp4`,
47
48
  );
48
49
  });
50
+
51
+ test("Honors provided apiHost", () => {
52
+ const preview = document.createElement("ef-preview");
53
+
54
+ const id = v4();
55
+ const target = document.createElement("ef-video");
56
+ target.setAttribute("id", id);
57
+ target.assetId = `${id}:example.mp4`;
58
+ document.body.appendChild(target);
59
+ const captions = document.createElement("ef-captions");
60
+ captions.setAttribute("target", id);
61
+ preview.appendChild(captions);
62
+ document.body.appendChild(preview);
63
+ preview.apiHost = "test://";
64
+ expect(captions.captionsPath()).toBe(
65
+ `test:///api/v1/caption_files/${id}:example.mp4`,
66
+ );
67
+ });
49
68
  });
50
69
  });
@@ -93,7 +93,7 @@ export class EFCaptions extends EFSourceMixin(
93
93
  if (EF_RENDERING()) {
94
94
  return `editframe://api/v1/caption_files/${this.targetElement.assetId}`;
95
95
  }
96
- return `https://editframe.dev/api/v1/caption_files/${this.targetElement.assetId}`;
96
+ return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
97
97
  }
98
98
  const targetSrc = this.targetElement.src;
99
99
  return `/@ef-captions/${targetSrc}`;
@@ -1,5 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
2
  import "./EFImage.ts";
3
+ import "../gui/EFPreview.ts";
3
4
  import { v4 } from "uuid";
4
5
 
5
6
  describe("EFImage", () => {
@@ -44,5 +45,18 @@ describe("EFImage", () => {
44
45
  `https://editframe.dev/api/v1/image_files/${id}:example.jpg`,
45
46
  );
46
47
  });
48
+
49
+ test("honors apiHost", () => {
50
+ const id = v4();
51
+ const image = document.createElement("ef-image");
52
+ const preview = document.createElement("ef-preview");
53
+ image.setAttribute("asset-id", `${id}:example.jpg`);
54
+ preview.appendChild(image);
55
+ preview.apiHost = "test://";
56
+ document.body.appendChild(preview);
57
+ expect(image.assetPath()).toBe(
58
+ `test:///api/v1/image_files/${id}:example.jpg`,
59
+ );
60
+ });
47
61
  });
48
62
  });
@@ -54,7 +54,7 @@ export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
54
54
  if (EF_RENDERING()) {
55
55
  return `editframe://api/v1/image_files/${this.assetId}`;
56
56
  }
57
- return `https://editframe.dev/api/v1/image_files/${this.assetId}`;
57
+ return `${this.apiHost}/api/v1/image_files/${this.assetId}`;
58
58
  }
59
59
  return `/@ef-image/${this.src}`;
60
60
  }
@@ -3,6 +3,7 @@ import { v4 } from "uuid";
3
3
  import { customElement } from "lit/decorators.js";
4
4
  import { EFMedia } from "./EFMedia.ts";
5
5
  import "../gui/EFWorkbench.ts";
6
+ import "../gui/EFPreview.ts";
6
7
 
7
8
  @customElement("test-media")
8
9
  class TestMedia extends EFMedia {}
@@ -79,5 +80,31 @@ describe("EFMedia", () => {
79
80
  `https://editframe.dev/api/v1/isobmff_tracks/${id}:example.mp4/1`,
80
81
  );
81
82
  });
83
+
84
+ test("honors apiHost in fragmentIndexPath", () => {
85
+ const id = v4();
86
+ const element = document.createElement("test-media");
87
+ element.setAttribute("asset-id", `${id}:example.mp4`);
88
+ const preview = document.createElement("ef-preview");
89
+ preview.appendChild(element);
90
+ preview.apiHost = "test://";
91
+ document.body.appendChild(preview);
92
+ expect(element.fragmentIndexPath()).toBe(
93
+ `test:///api/v1/isobmff_files/${id}:example.mp4/index`,
94
+ );
95
+ });
96
+
97
+ test("honors apiHost in fragmentTrackPath", () => {
98
+ const id = v4();
99
+ const element = document.createElement("test-media");
100
+ element.setAttribute("asset-id", `${id}:example.mp4`);
101
+ const preview = document.createElement("ef-preview");
102
+ preview.appendChild(element);
103
+ preview.apiHost = "test://";
104
+ document.body.appendChild(preview);
105
+ expect(element.fragmentTrackPath("1")).toBe(
106
+ `test:///api/v1/isobmff_tracks/${id}:example.mp4/1`,
107
+ );
108
+ });
82
109
  });
83
110
  });
@@ -74,7 +74,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
74
74
  if (EF_RENDERING()) {
75
75
  return `editframe://api/v1/isobmff_files/${this.assetId}/index`;
76
76
  }
77
- return `https://editframe.dev/api/v1/isobmff_files/${this.assetId}/index`;
77
+ return `${this.apiHost}/api/v1/isobmff_files/${this.assetId}/index`;
78
78
  }
79
79
  return `/@ef-track-fragment-index/${this.src ?? ""}`;
80
80
  }
@@ -84,7 +84,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
84
84
  if (EF_RENDERING()) {
85
85
  return `editframe://api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
86
86
  }
87
- return `https://editframe.dev/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
87
+ return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
88
88
  }
89
89
  // trackId is only specified as a query in the @ef-track url shape
90
90
  // this is because that system doesn't have a full url matching system.
@@ -6,6 +6,7 @@ import { property } from "lit/decorators/property.js";
6
6
  import { apiHostContext } from "../gui/apiHostContext.ts";
7
7
 
8
8
  export declare class EFSourceMixinInterface {
9
+ apiHost?: string;
9
10
  productionSrc(): string;
10
11
  src: string;
11
12
  }
@@ -21,7 +22,11 @@ export function EFSourceMixin<T extends Constructor<LitElement>>(
21
22
  class EFSourceElement extends superClass {
22
23
  @consume({ context: apiHostContext, subscribe: true })
23
24
  @state()
24
- private efHost?: string;
25
+ private _apiHost?: string;
26
+
27
+ get apiHost() {
28
+ return this._apiHost ?? "https://editframe.dev";
29
+ }
25
30
 
26
31
  @property({ type: String })
27
32
  src = "";
@@ -33,13 +38,13 @@ export function EFSourceMixin<T extends Constructor<LitElement>>(
33
38
  );
34
39
  }
35
40
 
36
- if (!this.efHost) {
41
+ if (!this.apiHost) {
37
42
  throw new Error(
38
- `efHost not available for ${this}. Cannot generate production URL`,
43
+ `apiHost not available for ${this}. Cannot generate production URL`,
39
44
  );
40
45
  }
41
46
 
42
- return `${this.efHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;
47
+ return `${this.apiHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;
43
48
  }
44
49
 
45
50
  md5SumLoader = new Task(this, {
@@ -0,0 +1,81 @@
1
+ import { LitElement } from "lit";
2
+ import { customElement } from "lit/decorators/custom-element.js";
3
+ import { describe, expect, test, vi } from "vitest";
4
+
5
+ import { ContextMixin } from "./ContextMixin.ts";
6
+ import { consume } from "@lit/context";
7
+ import { apiHostContext } from "./apiHostContext.ts";
8
+
9
+ @customElement("test-context")
10
+ class TestContext extends ContextMixin(LitElement) {}
11
+
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ "test-context": TestContext;
15
+ }
16
+ }
17
+
18
+ @customElement("ef-host-consumer")
19
+ class EFHostConsumer extends LitElement {
20
+ @consume({ context: apiHostContext, subscribe: true })
21
+ apiHost?: string;
22
+ }
23
+
24
+ declare global {
25
+ interface HTMLElementTagNameMap {
26
+ "ef-host-consumer": EFHostConsumer;
27
+ }
28
+ }
29
+
30
+ describe("ContextMixin", () => {
31
+ test("should be defined", () => {
32
+ expect(ContextMixin).toBeDefined();
33
+ });
34
+
35
+ describe("efHost", () => {
36
+ test("Provides apiHost", () => {
37
+ const element = document.createElement("test-context");
38
+ const consumer = document.createElement("ef-host-consumer");
39
+ document.body.appendChild(element);
40
+ element.appendChild(consumer);
41
+ expect(consumer.apiHost).toBe(element.apiHost);
42
+
43
+ element.apiHost = "test";
44
+ expect(consumer.apiHost).toBe("test");
45
+
46
+ element.setAttribute("api-host", "test2");
47
+ expect(consumer.apiHost).toBe("test2");
48
+
49
+ expect(element.apiHost).toBe("test2");
50
+ });
51
+ });
52
+
53
+ describe("Playback", () => {
54
+ test("should start playback", () => {
55
+ const element = document.createElement("test-context");
56
+ element.playing = true;
57
+ expect(element.playing).toBe(true);
58
+ });
59
+
60
+ test("playback starts immediately if connected", () => {
61
+ const element = document.createElement("test-context");
62
+ // @ts-expect-error startPlayback is private
63
+ const playbackSpy = vi.spyOn(element, "startPlayback");
64
+ element.playing = true;
65
+ expect(element.playing).toBe(true);
66
+ document.body.appendChild(element);
67
+ expect(playbackSpy).toHaveBeenCalled();
68
+ });
69
+
70
+ test("playback stops immediately if disconnected", () => {
71
+ const element = document.createElement("test-context");
72
+ element.playing = true;
73
+ expect(element.playing).toBe(true);
74
+ document.body.appendChild(element);
75
+ // @ts-expect-error stopPlayback is private
76
+ const playbackSpy = vi.spyOn(element, "stopPlayback");
77
+ document.body.removeChild(element);
78
+ expect(playbackSpy).toHaveBeenCalled();
79
+ });
80
+ });
81
+ });
@@ -9,9 +9,11 @@ import { createRef } from "lit/directives/ref.js";
9
9
  import { loopContext, playingContext } from "./playingContext.ts";
10
10
  import type { EFTimegroup } from "../elements/EFTimegroup.ts";
11
11
  import { efContext } from "./efContext.ts";
12
+ import { apiHostContext } from "./apiHostContext.ts";
12
13
 
13
14
  export declare class ContextMixinInterface {
14
15
  signingURL?: string;
16
+ apiHost?: string;
15
17
  rendering: boolean;
16
18
  playing: boolean;
17
19
  loop: boolean;
@@ -34,6 +36,10 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
34
36
  @state()
35
37
  focusedElement?: HTMLElement;
36
38
 
39
+ @provide({ context: apiHostContext })
40
+ @property({ type: String, reflect: true, attribute: "api-host" })
41
+ apiHost?: string;
42
+
37
43
  @provide({ context: efContext })
38
44
  efContext = this;
39
45
 
@@ -140,14 +146,22 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
140
146
  // Preferrably we would use a resizeObserver, but it is difficult to get the first resize
141
147
  // timed correctly. So we use requestAnimationFrame as a stop-gap.
142
148
  requestAnimationFrame(this.setStageScale);
149
+ if (this.playing) {
150
+ this.startPlayback();
151
+ }
152
+ }
153
+
154
+ disconnectedCallback(): void {
155
+ super.disconnectedCallback();
156
+ this.stopPlayback();
143
157
  }
144
158
 
145
159
  update(changedProperties: Map<string | number | symbol, unknown>) {
146
160
  if (changedProperties.has("playing")) {
147
161
  if (this.playing) {
148
- this.#startPlayback();
162
+ this.startPlayback();
149
163
  } else {
150
- this.#stopPlayback();
164
+ this.stopPlayback();
151
165
  }
152
166
  }
153
167
 
@@ -183,7 +197,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
183
197
  });
184
198
  }
185
199
 
186
- async #stopPlayback() {
200
+ private async stopPlayback() {
187
201
  if (this.#playbackAudioContext) {
188
202
  if (this.#playbackAudioContext.state !== "closed") {
189
203
  await this.#playbackAudioContext.close();
@@ -195,23 +209,33 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
195
209
  this.#playbackAudioContext = null;
196
210
  }
197
211
 
198
- async #startPlayback() {
199
- await this.#stopPlayback();
212
+ private async startPlayback() {
213
+ await this.stopPlayback();
200
214
  const timegroup = this.targetTimegroup;
201
215
  if (!timegroup) {
202
216
  return;
203
217
  }
204
218
 
219
+ await timegroup.waitForMediaDurations();
220
+
205
221
  let currentMs = timegroup.currentTimeMs;
206
222
  let bufferCount = 0;
207
223
  this.#playbackAudioContext = new AudioContext({
208
224
  latencyHint: "playback",
209
225
  });
226
+
210
227
  if (this.#playbackAnimationFrameRequest) {
211
228
  cancelAnimationFrame(this.#playbackAnimationFrameRequest);
212
229
  }
213
230
  this.#syncPlayheadToAudioContext(timegroup, currentMs);
214
231
  const playbackContext = this.#playbackAudioContext;
232
+ if (playbackContext.state === "suspended") {
233
+ console.warn(
234
+ "AudioContext is suspended, media playback will not work until user has interacted with page.",
235
+ );
236
+ this.playing = false;
237
+ return;
238
+ }
215
239
  await playbackContext.suspend();
216
240
 
217
241
  const fillBuffer = async () => {
@@ -1,3 +1,5 @@
1
1
  import { createContext } from "@lit/context";
2
2
 
3
- export const apiHostContext = createContext<string>(Symbol("apiHostContext"));
3
+ export const apiHostContext = createContext<string | undefined>(
4
+ Symbol("apiHostContext"),
5
+ );