@editframe/elements 0.16.7-beta.0 → 0.17.6-beta.0

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 (101) hide show
  1. package/README.md +30 -0
  2. package/dist/DecoderResetFrequency.test.d.ts +1 -0
  3. package/dist/DecoderResetRecovery.test.d.ts +1 -0
  4. package/dist/DelayedLoadingState.d.ts +48 -0
  5. package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
  6. package/dist/DelayedLoadingState.js +113 -0
  7. package/dist/DelayedLoadingState.test.d.ts +1 -0
  8. package/dist/EF_FRAMEGEN.d.ts +10 -1
  9. package/dist/EF_FRAMEGEN.js +199 -179
  10. package/dist/EF_INTERACTIVE.js +2 -6
  11. package/dist/EF_RENDERING.js +1 -3
  12. package/dist/JitTranscodingClient.browsertest.d.ts +1 -0
  13. package/dist/JitTranscodingClient.d.ts +167 -0
  14. package/dist/JitTranscodingClient.js +373 -0
  15. package/dist/JitTranscodingClient.test.d.ts +1 -0
  16. package/dist/LoadingDebounce.test.d.ts +1 -0
  17. package/dist/LoadingIndicator.browsertest.d.ts +0 -0
  18. package/dist/ManualScrubTest.test.d.ts +1 -0
  19. package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
  20. package/dist/ScrubTrackIntegration.test.d.ts +1 -0
  21. package/dist/ScrubTrackManager.d.ts +96 -0
  22. package/dist/ScrubTrackManager.js +216 -0
  23. package/dist/ScrubTrackManager.test.d.ts +1 -0
  24. package/dist/SegmentSwitchLoading.test.d.ts +1 -0
  25. package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
  26. package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
  27. package/dist/elements/CrossUpdateController.js +13 -15
  28. package/dist/elements/EFAudio.browsertest.d.ts +0 -0
  29. package/dist/elements/EFAudio.d.ts +1 -1
  30. package/dist/elements/EFAudio.js +30 -43
  31. package/dist/elements/EFCaptions.js +337 -373
  32. package/dist/elements/EFImage.js +64 -90
  33. package/dist/elements/EFMedia.d.ts +98 -33
  34. package/dist/elements/EFMedia.js +1169 -678
  35. package/dist/elements/EFSourceMixin.js +31 -48
  36. package/dist/elements/EFTemporal.d.ts +1 -0
  37. package/dist/elements/EFTemporal.js +266 -360
  38. package/dist/elements/EFTimegroup.d.ts +3 -1
  39. package/dist/elements/EFTimegroup.js +262 -323
  40. package/dist/elements/EFVideo.browsertest.d.ts +0 -0
  41. package/dist/elements/EFVideo.d.ts +90 -2
  42. package/dist/elements/EFVideo.js +408 -111
  43. package/dist/elements/EFWaveform.js +375 -411
  44. package/dist/elements/FetchMixin.js +14 -24
  45. package/dist/elements/MediaController.d.ts +30 -0
  46. package/dist/elements/TargetController.js +130 -156
  47. package/dist/elements/TimegroupController.js +17 -19
  48. package/dist/elements/durationConverter.js +15 -4
  49. package/dist/elements/parseTimeToMs.js +4 -10
  50. package/dist/elements/printTaskStatus.d.ts +2 -0
  51. package/dist/elements/printTaskStatus.js +11 -0
  52. package/dist/elements/updateAnimations.js +39 -59
  53. package/dist/getRenderInfo.js +58 -67
  54. package/dist/gui/ContextMixin.js +203 -288
  55. package/dist/gui/EFConfiguration.js +27 -43
  56. package/dist/gui/EFFilmstrip.js +440 -620
  57. package/dist/gui/EFFitScale.js +112 -135
  58. package/dist/gui/EFFocusOverlay.js +45 -61
  59. package/dist/gui/EFPreview.js +30 -49
  60. package/dist/gui/EFScrubber.js +78 -99
  61. package/dist/gui/EFTimeDisplay.js +49 -70
  62. package/dist/gui/EFToggleLoop.js +17 -34
  63. package/dist/gui/EFTogglePlay.js +37 -58
  64. package/dist/gui/EFWorkbench.js +66 -88
  65. package/dist/gui/TWMixin.js +2 -48
  66. package/dist/gui/TWMixin2.js +31 -0
  67. package/dist/gui/efContext.js +2 -6
  68. package/dist/gui/fetchContext.js +1 -3
  69. package/dist/gui/focusContext.js +1 -3
  70. package/dist/gui/focusedElementContext.js +2 -6
  71. package/dist/gui/playingContext.js +1 -4
  72. package/dist/index.js +5 -30
  73. package/dist/msToTimeCode.js +11 -13
  74. package/dist/style.css +2 -1
  75. package/package.json +3 -3
  76. package/src/elements/EFAudio.browsertest.ts +569 -0
  77. package/src/elements/EFAudio.ts +4 -6
  78. package/src/elements/EFCaptions.browsertest.ts +0 -1
  79. package/src/elements/EFImage.browsertest.ts +0 -1
  80. package/src/elements/EFMedia.browsertest.ts +147 -115
  81. package/src/elements/EFMedia.ts +1339 -307
  82. package/src/elements/EFTemporal.browsertest.ts +0 -1
  83. package/src/elements/EFTemporal.ts +11 -0
  84. package/src/elements/EFTimegroup.ts +73 -10
  85. package/src/elements/EFVideo.browsertest.ts +680 -0
  86. package/src/elements/EFVideo.ts +729 -50
  87. package/src/elements/EFWaveform.ts +4 -4
  88. package/src/elements/MediaController.ts +108 -0
  89. package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
  90. package/src/elements/printTaskStatus.ts +16 -0
  91. package/src/elements/updateAnimations.ts +6 -0
  92. package/src/gui/TWMixin.ts +10 -3
  93. package/test/EFVideo.frame-tasks.browsertest.ts +524 -0
  94. package/test/EFVideo.framegen.browsertest.ts +118 -0
  95. package/test/createJitTestClips.ts +293 -0
  96. package/test/useAssetMSW.ts +49 -0
  97. package/test/useMSW.ts +31 -0
  98. package/types.json +1 -1
  99. package/dist/gui/TWMixin.css.js +0 -4
  100. /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
  101. /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
@@ -1,72 +1,63 @@
1
1
  import { z } from "zod";
2
2
  const RenderInfo = z.object({
3
- width: z.number().positive(),
4
- height: z.number().positive(),
5
- fps: z.number().positive(),
6
- durationMs: z.number().positive(),
7
- assets: z.object({
8
- efMedia: z.record(z.any()),
9
- efCaptions: z.array(z.string()),
10
- efImage: z.array(z.string())
11
- })
3
+ width: z.number().positive(),
4
+ height: z.number().positive(),
5
+ fps: z.number().positive(),
6
+ durationMs: z.number().positive(),
7
+ assets: z.object({
8
+ efMedia: z.record(z.any()),
9
+ efCaptions: z.array(z.string()),
10
+ efImage: z.array(z.string())
11
+ })
12
12
  });
13
13
  const getRenderInfo = async () => {
14
- const rootTimeGroup = document.querySelector("ef-timegroup");
15
- if (!rootTimeGroup) {
16
- throw new Error("No ef-timegroup found");
17
- }
18
- console.error("Waiting for media durations", rootTimeGroup);
19
- await rootTimeGroup.waitForMediaDurations();
20
- const width = rootTimeGroup.clientWidth;
21
- const height = rootTimeGroup.clientHeight;
22
- const fps = 30;
23
- const durationMs = Math.round(rootTimeGroup.durationMs);
24
- const elements = document.querySelectorAll(
25
- "ef-audio, ef-video, ef-image, ef-captions"
26
- );
27
- const assets = {
28
- efMedia: {},
29
- efCaptions: /* @__PURE__ */ new Set(),
30
- efImage: /* @__PURE__ */ new Set()
31
- };
32
- for (const element of elements) {
33
- switch (element.tagName) {
34
- case "EF-AUDIO":
35
- case "EF-VIDEO": {
36
- const src = element.src;
37
- console.error("Processing element", element.tagName, src);
38
- assets.efMedia[src] = element.trackFragmentIndexLoader.value;
39
- break;
40
- }
41
- case "EF-IMAGE": {
42
- const src = element.src;
43
- console.error("Processing element", element.tagName, src);
44
- assets.efImage.add(src);
45
- break;
46
- }
47
- case "EF-CAPTIONS": {
48
- const src = element.targetElement?.src;
49
- console.error("Processing element", element.tagName, src);
50
- assets.efCaptions.add(src);
51
- break;
52
- }
53
- }
54
- }
55
- const renderInfo = {
56
- width,
57
- height,
58
- fps,
59
- durationMs,
60
- assets: {
61
- efMedia: assets.efMedia,
62
- efCaptions: Array.from(assets.efCaptions),
63
- efImage: Array.from(assets.efImage)
64
- }
65
- };
66
- console.error("Render info", renderInfo);
67
- return renderInfo;
68
- };
69
- export {
70
- RenderInfo,
71
- getRenderInfo
14
+ const rootTimeGroup = document.querySelector("ef-timegroup");
15
+ if (!rootTimeGroup) throw new Error("No ef-timegroup found");
16
+ console.error("Waiting for media durations", rootTimeGroup);
17
+ await rootTimeGroup.waitForMediaDurations();
18
+ const width = rootTimeGroup.clientWidth;
19
+ const height = rootTimeGroup.clientHeight;
20
+ const fps = 30;
21
+ const durationMs = Math.round(rootTimeGroup.durationMs);
22
+ const elements = document.querySelectorAll("ef-audio, ef-video, ef-image, ef-captions");
23
+ const assets = {
24
+ efMedia: {},
25
+ efCaptions: /* @__PURE__ */ new Set(),
26
+ efImage: /* @__PURE__ */ new Set()
27
+ };
28
+ for (const element of elements) switch (element.tagName) {
29
+ case "EF-AUDIO":
30
+ case "EF-VIDEO": {
31
+ const src = element.src;
32
+ console.error("Processing element", element.tagName, src);
33
+ assets.efMedia[src] = element.fragmentIndexTask.value;
34
+ break;
35
+ }
36
+ case "EF-IMAGE": {
37
+ const src = element.src;
38
+ console.error("Processing element", element.tagName, src);
39
+ assets.efImage.add(src);
40
+ break;
41
+ }
42
+ case "EF-CAPTIONS": {
43
+ const src = element.targetElement?.src;
44
+ console.error("Processing element", element.tagName, src);
45
+ assets.efCaptions.add(src);
46
+ break;
47
+ }
48
+ }
49
+ const renderInfo = {
50
+ width,
51
+ height,
52
+ fps,
53
+ durationMs,
54
+ assets: {
55
+ efMedia: assets.efMedia,
56
+ efCaptions: Array.from(assets.efCaptions),
57
+ efImage: Array.from(assets.efImage)
58
+ }
59
+ };
60
+ console.error("Render info", renderInfo);
61
+ return renderInfo;
72
62
  };
63
+ export { RenderInfo, getRenderInfo };
@@ -1,298 +1,213 @@
1
- import { createContext, consume, provide } from "@lit/context";
2
- import { state, property } from "lit/decorators.js";
3
1
  import { efConfigurationContext } from "./EFConfiguration.js";
4
2
  import { efContext } from "./efContext.js";
5
3
  import { fetchContext } from "./fetchContext.js";
6
4
  import { focusContext } from "./focusContext.js";
7
5
  import { focusedElementContext } from "./focusedElementContext.js";
8
- import { playingContext, loopContext } from "./playingContext.js";
9
- var __defProp = Object.defineProperty;
10
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
- var __decorateClass = (decorators, target, key, kind) => {
12
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
13
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
14
- if (decorator = decorators[i])
15
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
16
- if (kind && result) __defProp(target, key, result);
17
- return result;
18
- };
19
- const targetTimegroupContext = createContext(
20
- "target-timegroup"
21
- );
6
+ import { loopContext, playingContext } from "./playingContext.js";
7
+ import { consume, createContext, provide } from "@lit/context";
8
+ import { property, state } from "lit/decorators.js";
9
+ import _decorate from "@oxc-project/runtime/helpers/decorate";
10
+ const targetTimegroupContext = createContext("target-timegroup");
22
11
  const contextMixinSymbol = Symbol("contextMixin");
23
12
  function isContextMixin(value) {
24
- return typeof value === "object" && value !== null && contextMixinSymbol in value.constructor;
13
+ return typeof value === "object" && value !== null && contextMixinSymbol in value.constructor;
25
14
  }
26
15
  function ContextMixin(superClass) {
27
- var _a, _b;
28
- class ContextElement extends (_b = superClass, _a = contextMixinSymbol, _b) {
29
- constructor() {
30
- super(...arguments);
31
- this.efConfiguration = null;
32
- this.focusContext = this;
33
- this.efContext = this;
34
- this.targetTimegroup = null;
35
- this.fetch = async (url, init = {}) => {
36
- init.headers ||= {};
37
- Object.assign(init.headers, {
38
- "Content-Type": "application/json"
39
- });
40
- if (this.signingURL) {
41
- if (!this.#URLTokens[url]) {
42
- this.#URLTokens[url] = fetch(this.signingURL, {
43
- method: "POST",
44
- body: JSON.stringify({ url })
45
- }).then(async (response) => {
46
- if (response.ok) {
47
- return (await response.json()).token;
48
- }
49
- throw new Error(
50
- `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`
51
- );
52
- });
53
- }
54
- const urlToken = await this.#URLTokens[url];
55
- Object.assign(init.headers, {
56
- authorization: `Bearer ${urlToken}`
57
- });
58
- } else {
59
- init.credentials = "include";
60
- }
61
- return fetch(url, init);
62
- };
63
- this.#URLTokens = {};
64
- this.playing = false;
65
- this.loop = false;
66
- this.rendering = false;
67
- this.currentTimeMs = 0;
68
- this.#FPS = 30;
69
- this.#MS_PER_FRAME = 1e3 / this.#FPS;
70
- this.#timegroupObserver = new MutationObserver((mutations) => {
71
- for (const mutation of mutations) {
72
- if (mutation.type === "childList") {
73
- const newTimegroup = this.querySelector("ef-timegroup");
74
- if (newTimegroup !== this.targetTimegroup) {
75
- this.targetTimegroup = newTimegroup;
76
- }
77
- }
78
- }
79
- });
80
- this.#playbackAudioContext = null;
81
- this.#playbackAnimationFrameRequest = null;
82
- this.#AUDIO_PLAYBACK_SLICE_MS = 1e3;
83
- }
84
- static {
85
- this[_a] = true;
86
- }
87
- #apiHost;
88
- get apiHost() {
89
- return this.#apiHost ?? this.efConfiguration?.apiHost ?? "";
90
- }
91
- set apiHost(value) {
92
- this.#apiHost = value;
93
- }
94
- #URLTokens;
95
- #signingURL;
96
- get signingURL() {
97
- return this.#signingURL ?? this.efConfiguration?.signingURL ?? "";
98
- }
99
- set signingURL(value) {
100
- this.#signingURL = value;
101
- }
102
- #FPS;
103
- #MS_PER_FRAME;
104
- #timegroupObserver;
105
- connectedCallback() {
106
- super.connectedCallback();
107
- this.targetTimegroup = this.querySelector("ef-timegroup");
108
- this.#timegroupObserver.observe(this, {
109
- childList: true,
110
- subtree: true,
111
- attributes: true
112
- });
113
- if (this.playing) {
114
- this.startPlayback();
115
- }
116
- }
117
- disconnectedCallback() {
118
- super.disconnectedCallback();
119
- this.#timegroupObserver.disconnect();
120
- this.stopPlayback();
121
- }
122
- update(changedProperties) {
123
- if (changedProperties.has("playing")) {
124
- if (this.playing) {
125
- this.startPlayback();
126
- } else {
127
- this.stopPlayback();
128
- }
129
- }
130
- if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
131
- if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
132
- this.targetTimegroup.currentTimeMs = this.currentTimeMs;
133
- if (this.isConnected) {
134
- this.dispatchEvent(
135
- new CustomEvent("timeupdate", {
136
- detail: {
137
- currentTimeMs: this.currentTimeMs,
138
- progress: this.currentTimeMs / this.targetTimegroup.durationMs
139
- }
140
- })
141
- );
142
- }
143
- }
144
- }
145
- super.update(changedProperties);
146
- }
147
- play() {
148
- this.playing = true;
149
- }
150
- pause() {
151
- this.playing = false;
152
- }
153
- #playbackAudioContext;
154
- #playbackAnimationFrameRequest;
155
- #AUDIO_PLAYBACK_SLICE_MS;
156
- #syncPlayheadToAudioContext(target, startMs) {
157
- const rawTimeMs = startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1e3;
158
- const nextTimeMs = Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
159
- if (nextTimeMs !== this.currentTimeMs) {
160
- this.currentTimeMs = nextTimeMs;
161
- }
162
- this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
163
- this.#syncPlayheadToAudioContext(target, startMs);
164
- });
165
- }
166
- async stopPlayback() {
167
- if (this.#playbackAudioContext) {
168
- if (this.#playbackAudioContext.state !== "closed") {
169
- await this.#playbackAudioContext.close();
170
- }
171
- }
172
- if (this.#playbackAnimationFrameRequest) {
173
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
174
- }
175
- this.#playbackAudioContext = null;
176
- }
177
- async startPlayback() {
178
- await this.stopPlayback();
179
- const timegroup = this.targetTimegroup;
180
- if (!timegroup) {
181
- return;
182
- }
183
- await timegroup.waitForMediaDurations();
184
- let currentMs = timegroup.currentTimeMs;
185
- const fromMs = currentMs;
186
- const toMs = timegroup.endTimeMs;
187
- if (fromMs >= toMs) {
188
- this.pause();
189
- return;
190
- }
191
- let bufferCount = 0;
192
- this.#playbackAudioContext = new AudioContext({
193
- latencyHint: "playback"
194
- });
195
- if (this.#playbackAnimationFrameRequest) {
196
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
197
- }
198
- this.#syncPlayheadToAudioContext(timegroup, currentMs);
199
- const playbackContext = this.#playbackAudioContext;
200
- if (playbackContext.state === "suspended") {
201
- console.warn(
202
- "AudioContext is suspended, media playback will not work until user has interacted with page."
203
- );
204
- this.playing = false;
205
- return;
206
- }
207
- await playbackContext.suspend();
208
- const fillBuffer = async () => {
209
- if (bufferCount > 1) {
210
- return;
211
- }
212
- const canFillBuffer = await queueBufferSource();
213
- if (canFillBuffer) {
214
- fillBuffer();
215
- }
216
- };
217
- const queueBufferSource = async () => {
218
- if (currentMs >= toMs) {
219
- return false;
220
- }
221
- const startMs = currentMs;
222
- const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
223
- currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
224
- const audioBuffer = await timegroup.renderAudio(startMs, endMs);
225
- bufferCount++;
226
- const source = playbackContext.createBufferSource();
227
- source.buffer = audioBuffer;
228
- source.connect(playbackContext.destination);
229
- source.start((startMs - fromMs) / 1e3);
230
- source.onended = () => {
231
- bufferCount--;
232
- if (endMs >= toMs) {
233
- this.pause();
234
- if (this.loop) {
235
- this.updateComplete.then(() => {
236
- this.currentTimeMs = 0;
237
- this.updateComplete.then(() => {
238
- this.play();
239
- });
240
- });
241
- }
242
- } else {
243
- fillBuffer();
244
- }
245
- };
246
- return true;
247
- };
248
- await fillBuffer();
249
- await playbackContext.resume();
250
- }
251
- }
252
- __decorateClass([
253
- consume({ context: efConfigurationContext, subscribe: true })
254
- ], ContextElement.prototype, "efConfiguration", 2);
255
- __decorateClass([
256
- provide({ context: focusContext })
257
- ], ContextElement.prototype, "focusContext", 2);
258
- __decorateClass([
259
- provide({ context: focusedElementContext }),
260
- state()
261
- ], ContextElement.prototype, "focusedElement", 2);
262
- __decorateClass([
263
- property({ type: String, attribute: "api-host" })
264
- ], ContextElement.prototype, "apiHost", 1);
265
- __decorateClass([
266
- provide({ context: efContext })
267
- ], ContextElement.prototype, "efContext", 2);
268
- __decorateClass([
269
- provide({ context: targetTimegroupContext }),
270
- state()
271
- ], ContextElement.prototype, "targetTimegroup", 2);
272
- __decorateClass([
273
- provide({ context: fetchContext })
274
- ], ContextElement.prototype, "fetch", 2);
275
- __decorateClass([
276
- property({ type: String, attribute: "signing-url" })
277
- ], ContextElement.prototype, "signingURL", 1);
278
- __decorateClass([
279
- provide({ context: playingContext }),
280
- property({ type: Boolean, reflect: true })
281
- ], ContextElement.prototype, "playing", 2);
282
- __decorateClass([
283
- provide({ context: loopContext }),
284
- property({ type: Boolean, reflect: true })
285
- ], ContextElement.prototype, "loop", 2);
286
- __decorateClass([
287
- property({ type: Boolean })
288
- ], ContextElement.prototype, "rendering", 2);
289
- __decorateClass([
290
- state()
291
- ], ContextElement.prototype, "currentTimeMs", 2);
292
- return ContextElement;
16
+ class ContextElement extends superClass {
17
+ constructor(..._args) {
18
+ super(..._args);
19
+ this.efConfiguration = null;
20
+ this.focusContext = this;
21
+ this.efContext = this;
22
+ this.targetTimegroup = null;
23
+ this.fetch = async (url, init = {}) => {
24
+ init.headers ||= {};
25
+ Object.assign(init.headers, { "Content-Type": "application/json" });
26
+ if (this.signingURL) {
27
+ if (!this.#URLTokens[url]) this.#URLTokens[url] = fetch(this.signingURL, {
28
+ method: "POST",
29
+ body: JSON.stringify({ url })
30
+ }).then(async (response) => {
31
+ if (response.ok) return (await response.json()).token;
32
+ throw new Error(`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`);
33
+ });
34
+ const urlToken = await this.#URLTokens[url];
35
+ Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
36
+ } else init.credentials = "include";
37
+ return fetch(url, init);
38
+ };
39
+ this.playing = false;
40
+ this.loop = false;
41
+ this.rendering = false;
42
+ this.currentTimeMs = 0;
43
+ }
44
+ static {
45
+ this[contextMixinSymbol] = true;
46
+ }
47
+ #apiHost;
48
+ get apiHost() {
49
+ return this.#apiHost ?? this.efConfiguration?.apiHost ?? "";
50
+ }
51
+ set apiHost(value) {
52
+ this.#apiHost = value;
53
+ }
54
+ #URLTokens = {};
55
+ #signingURL;
56
+ /**
57
+ * A URL that will be used to generated signed tokens for accessing media files from the
58
+ * editframe API. This is used to authenticate media requests per-user.
59
+ */
60
+ get signingURL() {
61
+ return this.#signingURL ?? this.efConfiguration?.signingURL ?? "";
62
+ }
63
+ set signingURL(value) {
64
+ this.#signingURL = value;
65
+ }
66
+ #FPS = 30;
67
+ #MS_PER_FRAME = 1e3 / this.#FPS;
68
+ #timegroupObserver = new MutationObserver((mutations) => {
69
+ for (const mutation of mutations) if (mutation.type === "childList") {
70
+ const newTimegroup = this.querySelector("ef-timegroup");
71
+ if (newTimegroup !== this.targetTimegroup) this.targetTimegroup = newTimegroup;
72
+ }
73
+ });
74
+ connectedCallback() {
75
+ super.connectedCallback();
76
+ this.targetTimegroup = this.querySelector("ef-timegroup");
77
+ this.#timegroupObserver.observe(this, {
78
+ childList: true,
79
+ subtree: true,
80
+ attributes: true
81
+ });
82
+ if (this.playing) this.startPlayback();
83
+ }
84
+ disconnectedCallback() {
85
+ super.disconnectedCallback();
86
+ this.#timegroupObserver.disconnect();
87
+ this.stopPlayback();
88
+ }
89
+ update(changedProperties) {
90
+ if (changedProperties.has("playing")) if (this.playing) this.startPlayback();
91
+ else this.stopPlayback();
92
+ if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
93
+ if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
94
+ this.targetTimegroup.currentTimeMs = this.currentTimeMs;
95
+ if (this.isConnected) this.dispatchEvent(new CustomEvent("timeupdate", { detail: {
96
+ currentTimeMs: this.currentTimeMs,
97
+ progress: this.currentTimeMs / this.targetTimegroup.durationMs
98
+ } }));
99
+ }
100
+ }
101
+ super.update(changedProperties);
102
+ }
103
+ play() {
104
+ this.playing = true;
105
+ }
106
+ pause() {
107
+ this.playing = false;
108
+ }
109
+ #playbackAudioContext = null;
110
+ #playbackAnimationFrameRequest = null;
111
+ #AUDIO_PLAYBACK_SLICE_MS = 1e3;
112
+ #syncPlayheadToAudioContext(target, startMs) {
113
+ const rawTimeMs = startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1e3;
114
+ const nextTimeMs = Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
115
+ if (nextTimeMs !== this.currentTimeMs) this.currentTimeMs = nextTimeMs;
116
+ this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
117
+ this.#syncPlayheadToAudioContext(target, startMs);
118
+ });
119
+ }
120
+ async stopPlayback() {
121
+ if (this.#playbackAudioContext) {
122
+ if (this.#playbackAudioContext.state !== "closed") await this.#playbackAudioContext.close();
123
+ }
124
+ if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
125
+ this.#playbackAudioContext = null;
126
+ }
127
+ async startPlayback() {
128
+ await this.stopPlayback();
129
+ const timegroup = this.targetTimegroup;
130
+ if (!timegroup) return;
131
+ await timegroup.waitForMediaDurations();
132
+ let currentMs = timegroup.currentTimeMs;
133
+ const fromMs = currentMs;
134
+ const toMs = timegroup.endTimeMs;
135
+ if (fromMs >= toMs) {
136
+ this.pause();
137
+ return;
138
+ }
139
+ let bufferCount = 0;
140
+ this.#playbackAudioContext = new AudioContext({ latencyHint: "playback" });
141
+ if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
142
+ this.#syncPlayheadToAudioContext(timegroup, currentMs);
143
+ const playbackContext = this.#playbackAudioContext;
144
+ if (playbackContext.state === "suspended") {
145
+ console.warn("AudioContext is suspended, media playback will not work until user has interacted with page.");
146
+ this.playing = false;
147
+ return;
148
+ }
149
+ await playbackContext.suspend();
150
+ const fillBuffer = async () => {
151
+ if (bufferCount > 1) return;
152
+ const canFillBuffer = await queueBufferSource();
153
+ if (canFillBuffer) fillBuffer();
154
+ };
155
+ const queueBufferSource = async () => {
156
+ if (currentMs >= toMs) return false;
157
+ const startMs = currentMs;
158
+ const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
159
+ currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
160
+ const audioBuffer = await timegroup.renderAudio(startMs, endMs);
161
+ bufferCount++;
162
+ const source = playbackContext.createBufferSource();
163
+ source.buffer = audioBuffer;
164
+ source.connect(playbackContext.destination);
165
+ source.start((startMs - fromMs) / 1e3);
166
+ source.onended = () => {
167
+ bufferCount--;
168
+ if (endMs >= toMs) {
169
+ this.pause();
170
+ if (this.loop) this.updateComplete.then(() => {
171
+ this.currentTimeMs = 0;
172
+ this.updateComplete.then(() => {
173
+ this.play();
174
+ });
175
+ });
176
+ } else fillBuffer();
177
+ };
178
+ return true;
179
+ };
180
+ await fillBuffer();
181
+ await playbackContext.resume();
182
+ }
183
+ }
184
+ _decorate([consume({
185
+ context: efConfigurationContext,
186
+ subscribe: true
187
+ })], ContextElement.prototype, "efConfiguration", void 0);
188
+ _decorate([provide({ context: focusContext })], ContextElement.prototype, "focusContext", void 0);
189
+ _decorate([provide({ context: focusedElementContext }), state()], ContextElement.prototype, "focusedElement", void 0);
190
+ _decorate([property({
191
+ type: String,
192
+ attribute: "api-host"
193
+ })], ContextElement.prototype, "apiHost", null);
194
+ _decorate([provide({ context: efContext })], ContextElement.prototype, "efContext", void 0);
195
+ _decorate([provide({ context: targetTimegroupContext }), state()], ContextElement.prototype, "targetTimegroup", void 0);
196
+ _decorate([provide({ context: fetchContext })], ContextElement.prototype, "fetch", void 0);
197
+ _decorate([property({
198
+ type: String,
199
+ attribute: "signing-url"
200
+ })], ContextElement.prototype, "signingURL", null);
201
+ _decorate([provide({ context: playingContext }), property({
202
+ type: Boolean,
203
+ reflect: true
204
+ })], ContextElement.prototype, "playing", void 0);
205
+ _decorate([provide({ context: loopContext }), property({
206
+ type: Boolean,
207
+ reflect: true
208
+ })], ContextElement.prototype, "loop", void 0);
209
+ _decorate([property({ type: Boolean })], ContextElement.prototype, "rendering", void 0);
210
+ _decorate([state()], ContextElement.prototype, "currentTimeMs", void 0);
211
+ return ContextElement;
293
212
  }
294
- export {
295
- ContextMixin,
296
- isContextMixin,
297
- targetTimegroupContext
298
- };
213
+ export { ContextMixin, isContextMixin, targetTimegroupContext };