@editframe/elements 0.10.0-beta.3 → 0.10.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFSourceMixin.d.ts +1 -0
- package/dist/elements/src/elements/EFCaptions.js +1 -1
- package/dist/elements/src/elements/EFImage.js +1 -1
- package/dist/elements/src/elements/EFMedia.js +2 -2
- package/dist/elements/src/elements/EFSourceMixin.js +7 -4
- package/dist/elements/src/gui/ContextMixin.js +25 -5
- package/dist/elements/src/gui/apiHostContext.js +3 -1
- package/dist/gui/ContextMixin.browsertest.d.ts +18 -0
- package/dist/gui/ContextMixin.d.ts +1 -0
- package/dist/gui/apiHostContext.d.ts +1 -1
- package/package.json +2 -2
- package/src/elements/EFCaptions.browsertest.ts +19 -0
- package/src/elements/EFCaptions.ts +1 -1
- package/src/elements/EFImage.browsertest.ts +14 -0
- package/src/elements/EFImage.ts +1 -1
- package/src/elements/EFMedia.browsertest.ts +27 -0
- package/src/elements/EFMedia.ts +2 -2
- package/src/elements/EFSourceMixin.ts +9 -4
- package/src/gui/ContextMixin.browsertest.ts +81 -0
- package/src/gui/ContextMixin.ts +29 -5
- package/src/gui/apiHostContext.ts +3 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
39
|
+
if (!this.apiHost) {
|
|
37
40
|
throw new Error(
|
|
38
|
-
`
|
|
41
|
+
`apiHost not available for ${this}. Cannot generate production URL`
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
|
-
return `${this.
|
|
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, "
|
|
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
|
|
113
|
+
this.startPlayback();
|
|
106
114
|
} else {
|
|
107
|
-
this
|
|
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
|
|
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
|
|
147
|
-
await this
|
|
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");
|
|
@@ -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 {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.5",
|
|
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.
|
|
23
|
+
"@editframe/assets": "0.10.0-beta.5",
|
|
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
|
|
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
|
});
|
package/src/elements/EFImage.ts
CHANGED
|
@@ -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
|
|
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
|
});
|
package/src/elements/EFMedia.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
41
|
+
if (!this.apiHost) {
|
|
37
42
|
throw new Error(
|
|
38
|
-
`
|
|
43
|
+
`apiHost not available for ${this}. Cannot generate production URL`,
|
|
39
44
|
);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
return `${this.
|
|
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
|
+
});
|
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -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
|
|
162
|
+
this.startPlayback();
|
|
149
163
|
} else {
|
|
150
|
-
this
|
|
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
|
|
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
|
|
199
|
-
await this
|
|
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 () => {
|