@editframe/elements 0.8.0-beta.1 → 0.8.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +0 -1
  2. package/dist/elements/CrossUpdateController.d.ts +0 -1
  3. package/dist/elements/EFAudio.d.ts +0 -1
  4. package/dist/elements/EFCaptions.d.ts +3 -3
  5. package/dist/elements/EFImage.d.ts +0 -1
  6. package/dist/elements/EFMedia.d.ts +5 -3
  7. package/dist/elements/EFSourceMixin.d.ts +0 -1
  8. package/dist/elements/EFTemporal.d.ts +3 -1
  9. package/dist/elements/EFTimegroup.browsertest.d.ts +0 -1
  10. package/dist/elements/EFTimegroup.d.ts +2 -5
  11. package/dist/elements/EFVideo.d.ts +0 -1
  12. package/dist/elements/EFWaveform.d.ts +0 -1
  13. package/dist/elements/FetchMixin.d.ts +0 -1
  14. package/dist/elements/TimegroupController.d.ts +0 -1
  15. package/dist/elements/durationConverter.d.ts +8 -0
  16. package/dist/elements/src/elements/EFCaptions.js +10 -7
  17. package/dist/elements/src/elements/EFMedia.js +25 -8
  18. package/dist/elements/src/elements/EFTemporal.js +51 -2
  19. package/dist/elements/src/elements/EFTimegroup.js +22 -39
  20. package/dist/elements/src/elements/EFWaveform.js +3 -3
  21. package/dist/elements/src/elements/parseTimeToMs.js +1 -0
  22. package/dist/elements/src/gui/ContextMixin.js +37 -25
  23. package/dist/elements/src/gui/EFFilmstrip.js +27 -25
  24. package/dist/elements/src/gui/EFToggleLoop.js +39 -0
  25. package/dist/elements/src/gui/EFTogglePlay.js +43 -0
  26. package/dist/elements/src/gui/EFWorkbench.js +4 -4
  27. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  28. package/dist/elements/src/gui/efContext.js +7 -0
  29. package/dist/elements/src/gui/playingContext.js +2 -0
  30. package/dist/elements/src/index.js +4 -0
  31. package/dist/elements/util.d.ts +0 -1
  32. package/dist/gui/ContextMixin.d.ts +8 -12
  33. package/dist/gui/EFFilmstrip.d.ts +7 -3
  34. package/dist/gui/EFPreview.d.ts +1 -16
  35. package/dist/gui/EFToggleLoop.d.ts +12 -0
  36. package/dist/gui/EFTogglePlay.d.ts +12 -0
  37. package/dist/gui/EFWorkbench.d.ts +1 -17
  38. package/dist/gui/TWMixin.d.ts +0 -1
  39. package/dist/gui/efContext.d.ts +4 -0
  40. package/dist/gui/playingContext.d.ts +3 -0
  41. package/dist/index.d.ts +2 -1
  42. package/dist/style.css +7 -5
  43. package/package.json +3 -3
  44. package/src/elements/EFCaptions.ts +14 -8
  45. package/src/elements/EFMedia.ts +37 -9
  46. package/src/elements/EFTemporal.ts +60 -2
  47. package/src/elements/EFTimegroup.ts +21 -41
  48. package/src/elements/EFWaveform.ts +3 -3
  49. package/src/elements/durationConverter.ts +20 -0
  50. package/src/elements/parseTimeToMs.ts +1 -0
  51. package/src/gui/ContextMixin.ts +50 -36
  52. package/src/gui/EFFilmstrip.ts +27 -28
  53. package/src/gui/EFToggleLoop.ts +34 -0
  54. package/src/gui/EFTogglePlay.ts +38 -0
  55. package/src/gui/EFWorkbench.ts +4 -4
  56. package/src/gui/efContext.ts +6 -0
  57. package/src/gui/playingContext.ts +2 -0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  export { EFTimegroup } from './elements/EFTimegroup.ts';
3
2
  export { EFImage } from './elements/EFImage.ts';
4
3
  export type { EFMedia } from './elements/EFMedia.ts';
@@ -9,3 +8,5 @@ export { EFWaveform } from './elements/EFWaveform.ts';
9
8
  export { EFWorkbench } from './gui/EFWorkbench.ts';
10
9
  export { EFPreview } from './gui/EFPreview.ts';
11
10
  export { EFFilmstrip } from './gui/EFFilmstrip.ts';
11
+ export { EFTogglePlay } from './gui/EFTogglePlay.ts';
12
+ export { EFToggleLoop } from './gui/EFToggleLoop.ts';
package/dist/style.css CHANGED
@@ -624,6 +624,9 @@ video {
624
624
  .items-center {
625
625
  align-items: center;
626
626
  }
627
+ .justify-center {
628
+ justify-content: center;
629
+ }
627
630
  .overflow-auto {
628
631
  overflow: auto;
629
632
  }
@@ -675,6 +678,10 @@ video {
675
678
  --tw-bg-opacity: 1;
676
679
  background-color: rgb(226 232 240 / var(--tw-bg-opacity));
677
680
  }
681
+ .bg-slate-300 {
682
+ --tw-bg-opacity: 1;
683
+ background-color: rgb(203 213 225 / var(--tw-bg-opacity));
684
+ }
678
685
  .bg-opacity-20 {
679
686
  --tw-bg-opacity: 0.2;
680
687
  }
@@ -764,11 +771,6 @@ body {
764
771
  -webkit-text-size-adjust: 100%;
765
772
  }
766
773
 
767
- .hover\:bg-blue-400:hover {
768
- --tw-bg-opacity: 1;
769
- background-color: rgb(96 165 250 / var(--tw-bg-opacity));
770
- }
771
-
772
774
  .hover\:bg-slate-400:hover {
773
775
  --tw-bg-opacity: 1;
774
776
  background-color: rgb(148 163 184 / var(--tw-bg-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.8.0-beta.1",
3
+ "version": "0.8.0-beta.10",
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.8.0-beta.1",
23
+ "@editframe/assets": "0.8.0-beta.10",
24
24
  "@lit/context": "^1.1.2",
25
25
  "@lit/task": "^1.0.1",
26
26
  "d3": "^7.9.0",
@@ -35,7 +35,7 @@
35
35
  "autoprefixer": "^10.4.19",
36
36
  "rollup-plugin-tsconfig-paths": "^1.5.2",
37
37
  "typescript": "^5.5.4",
38
- "vite-plugin-dts": "^3.9.1",
38
+ "vite-plugin-dts": "^4.0.3",
39
39
  "vite-tsconfig-paths": "^4.3.2"
40
40
  }
41
41
  }
@@ -75,8 +75,12 @@ export class EFCaptions extends EFSourceMixin(
75
75
  `,
76
76
  ];
77
77
 
78
- @property({ type: String, attribute: "target" })
79
- target = null;
78
+ @property({ type: String, attribute: "target", reflect: true })
79
+ targetSelector = "";
80
+
81
+ set target(value: string) {
82
+ this.targetSelector = value;
83
+ }
80
84
 
81
85
  @property({ attribute: "word-style" })
82
86
  wordStyle = "";
@@ -149,13 +153,15 @@ export class EFCaptions extends EFSourceMixin(
149
153
  let endMs = 0;
150
154
  for (const segment of caption.segments) {
151
155
  if (
152
- this.targetElement.ownCurrentTimeMs >= segment.start * 1000 &&
153
- this.targetElement.ownCurrentTimeMs <= segment.end * 1000
156
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
157
+ segment.start * 1000 &&
158
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1000
154
159
  ) {
155
160
  for (const word of segment.words) {
156
161
  if (
157
- this.targetElement.ownCurrentTimeMs >= word.start * 1000 &&
158
- this.targetElement.ownCurrentTimeMs <= word.end * 1000
162
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
163
+ word.start * 1000 &&
164
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1000
159
165
  ) {
160
166
  words.push(word.text);
161
167
  startMs = word.start * 1000;
@@ -172,11 +178,11 @@ export class EFCaptions extends EFSourceMixin(
172
178
  }
173
179
 
174
180
  get targetElement() {
175
- const target = document.getElementById(this.getAttribute("target") ?? "");
181
+ const target = document.getElementById(this.targetSelector ?? "");
176
182
  if (target instanceof EFAudio || target instanceof EFVideo) {
177
183
  return target;
178
184
  }
179
- throw new Error("Invalid target, must be an EFAudio or EFVideo element");
185
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
180
186
  }
181
187
  }
182
188
 
@@ -145,7 +145,11 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
145
145
 
146
146
  const result: Record<
147
147
  string,
148
- { segment: TrackSegment; track: MP4Box.TrackInfo }
148
+ {
149
+ segment: TrackSegment;
150
+ track: MP4Box.TrackInfo;
151
+ nextSegment?: TrackSegment;
152
+ }
149
153
  > = {};
150
154
 
151
155
  for (const index of Object.values(fragmentIndex)) {
@@ -161,11 +165,15 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
161
165
  return (segment.dts / track.timescale) * 1000 <= seekToMs;
162
166
  });
163
167
 
168
+ const nextSegment = index.segments.find((segment) => {
169
+ return (segment.dts / track.timescale) * 1000 > seekToMs;
170
+ });
171
+
164
172
  if (!segment) {
165
173
  return;
166
174
  }
167
175
 
168
- result[index.track] = { segment, track };
176
+ result[index.track] = { segment, track, nextSegment };
169
177
  }
170
178
 
171
179
  return result;
@@ -187,7 +195,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
187
195
 
188
196
  const files: Record<string, File> = {};
189
197
 
190
- for (const [trackId, { segment, track }] of Object.entries(seekResult)) {
198
+ for (const [trackId, { segment, track, nextSegment }] of Object.entries(
199
+ seekResult,
200
+ )) {
191
201
  const start = segment.offset;
192
202
  const end = segment.offset + segment.size;
193
203
 
@@ -196,6 +206,21 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
196
206
  headers: { Range: `bytes=${start}-${end}` },
197
207
  });
198
208
 
209
+ if (nextSegment) {
210
+ const nextStart = nextSegment.offset;
211
+ const nextEnd = nextSegment.offset + nextSegment.size;
212
+ fetch(this.fragmentTrackPath(trackId), {
213
+ signal,
214
+ headers: { Range: `bytes=${nextStart}-${nextEnd}` },
215
+ })
216
+ .then(() => {
217
+ log("Prefetched next segment");
218
+ })
219
+ .catch((error) => {
220
+ log("Failed to prefetch next segment", error);
221
+ });
222
+ }
223
+
199
224
  const initSegment = Object.values(initSegments).find(
200
225
  (initSegment) => initSegment.trackId === String(track.id),
201
226
  );
@@ -253,7 +278,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
253
278
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
254
279
  ): void {
255
280
  if (changedProperties.has("ownCurrentTimeMs")) {
256
- this.executeSeek(this.ownCurrentTimeMs);
281
+ this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
257
282
  }
258
283
  }
259
284
 
@@ -274,7 +299,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
274
299
  if (durations.length === 0) {
275
300
  return 0;
276
301
  }
277
- return Math.max(...durations);
302
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
278
303
  }
279
304
 
280
305
  get startTimeMs() {
@@ -315,8 +340,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
315
340
 
316
341
  async fetchAudioSpanningTime(fromMs: number, toMs: number) {
317
342
  // Adjust range for track's own time
318
- fromMs -= this.startTimeMs;
319
- toMs -= this.startTimeMs;
343
+ fromMs -= this.startTimeMs - this.trimStartMs;
344
+ toMs -= this.startTimeMs - this.trimStartMs;
320
345
 
321
346
  await this.trackFragmentIndexLoader.taskComplete;
322
347
  const audioTrackId = this.defaultAudioTrackId;
@@ -383,10 +408,13 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
383
408
 
384
409
  return {
385
410
  blob: audioBlob,
386
- startMs: (firstFragment.dts / audioTrackIndex.timescale) * 1000,
411
+ startMs:
412
+ (firstFragment.dts / audioTrackIndex.timescale) * 1000 -
413
+ this.trimStartMs,
387
414
  endMs:
388
415
  (lastFragment.dts / audioTrackIndex.timescale) * 1000 +
389
- (lastFragment.duration / audioTrackIndex.timescale) * 1000,
416
+ (lastFragment.duration / audioTrackIndex.timescale) * 1000 -
417
+ this.trimEndMs,
390
418
  };
391
419
  }
392
420
  }
@@ -13,11 +13,14 @@ export const timegroupContext = createContext<EFTimegroup>(
13
13
 
14
14
  export declare class TemporalMixinInterface {
15
15
  get hasOwnDuration(): boolean;
16
+ get trimStartMs(): number;
17
+ get trimEndMs(): number;
16
18
  get durationMs(): number;
17
19
  get startTimeMs(): number;
18
20
  get startTimeWithinParentMs(): number;
19
21
  get endTimeMs(): number;
20
22
  get ownCurrentTimeMs(): number;
23
+ get trimAdjustedOwnCurrentTimeMs(): number;
21
24
 
22
25
  set duration(value: string);
23
26
  get duration(): string;
@@ -153,6 +156,36 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
153
156
  })
154
157
  private _durationMs?: number;
155
158
 
159
+ @property({
160
+ type: Number,
161
+ attribute: "trimstart",
162
+ converter: durationConverter,
163
+ })
164
+ private _trimStartMs = 0;
165
+ public get trimStartMs(): number {
166
+ return this._trimStartMs;
167
+ }
168
+
169
+ @property({
170
+ type: Number,
171
+ attribute: "trimend",
172
+ converter: durationConverter,
173
+ })
174
+ private _trimEndMs = 0;
175
+ public get trimEndMs(): number {
176
+ return this._trimEndMs;
177
+ }
178
+
179
+ @property({
180
+ type: Number,
181
+ attribute: "startoffset",
182
+ converter: durationConverter,
183
+ })
184
+ private _startOffsetMs = 0;
185
+ public get startOffsetMs(): number {
186
+ return this._startOffsetMs;
187
+ }
188
+
156
189
  @state()
157
190
  rootTimegroup?: EFTimegroup = this.getRootTimegroup();
158
191
 
@@ -220,9 +253,15 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
220
253
  }
221
254
  startTimeMsCache.set(
222
255
  this,
223
- previous.startTimeMs + previous.durationMs,
256
+ previous.startTimeMs +
257
+ previous.durationMs -
258
+ parentTimegroup.overlapMs,
259
+ );
260
+ return (
261
+ previous.startTimeMs +
262
+ previous.durationMs -
263
+ parentTimegroup.overlapMs
224
264
  );
225
- return previous.startTimeMs + previous.durationMs;
226
265
  }
227
266
  case "contain":
228
267
  case "fixed":
@@ -250,6 +289,25 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
250
289
  return 0;
251
290
  }
252
291
 
292
+ /**
293
+ * Used to calculate the internal currentTimeMs of the element. This is useful
294
+ * for mapping to internal media time codes for audio/video elements.
295
+ */
296
+ get trimAdjustedOwnCurrentTimeMs() {
297
+ if (this.rootTimegroup) {
298
+ return Math.min(
299
+ Math.max(
300
+ 0,
301
+ this.rootTimegroup.currentTimeMs -
302
+ this.startTimeMs +
303
+ this.trimStartMs,
304
+ ),
305
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs,
306
+ );
307
+ }
308
+ return 0;
309
+ }
310
+
253
311
  frameTask = new Task(this, {
254
312
  autoRun: EF_INTERACTIVE,
255
313
  args: () => [this.ownCurrentTimeMs] as const,
@@ -13,6 +13,7 @@ import {
13
13
  import { TimegroupController } from "./TimegroupController.ts";
14
14
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
15
  import { deepGetMediaElements } from "./EFMedia.ts";
16
+ import { durationConverter } from "./durationConverter.ts";
16
17
 
17
18
  const log = debug("ef:elements:EFTimegroup");
18
19
 
@@ -37,7 +38,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
37
38
  display: block;
38
39
  width: 100%;
39
40
  height: 100%;
40
- position: relative;
41
+ position: absolute;
41
42
  top: 0;
42
43
  }
43
44
  `;
@@ -53,6 +54,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
53
54
  })
54
55
  mode: "fixed" | "sequence" | "contain" = "sequence";
55
56
 
57
+ @property({
58
+ type: Number,
59
+ converter: durationConverter,
60
+ attribute: "overlap",
61
+ })
62
+ overlapMs = 0;
63
+
56
64
  @property({ type: Number })
57
65
  set currentTime(time: number) {
58
66
  this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
@@ -76,25 +84,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
76
84
  this.currentTime = ms / 1000;
77
85
  }
78
86
 
79
- @property({
80
- attribute: "crossover",
81
- converter: {
82
- fromAttribute: (value: string): number => {
83
- if (value.endsWith("ms")) {
84
- return Number.parseFloat(value);
85
- }
86
- if (value.endsWith("s")) {
87
- return Number.parseFloat(value) * 1000;
88
- }
89
- throw new Error(
90
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
91
- );
92
- },
93
- toAttribute: (value: number) => `${value}ms`,
94
- },
95
- })
96
- crossoverMs = 0;
97
-
98
87
  render() {
99
88
  return html`<slot></slot> `;
100
89
  }
@@ -134,32 +123,18 @@ export class EFTimegroup extends EFTemporal(LitElement) {
134
123
  return `ef-timegroup-${this.id}`;
135
124
  }
136
125
 
137
- get crossoverStartMs() {
138
- const parentTimeGroup = this.parentTimegroup;
139
- if (!parentTimeGroup || !this.previousElementSibling) {
140
- return 0;
141
- }
142
-
143
- return parentTimeGroup.crossoverMs;
144
- }
145
- get crossoverEndMs() {
146
- const parentTimeGroup = this.parentTimegroup;
147
- if (!parentTimeGroup || !this.nextElementSibling) {
148
- return 0;
149
- }
150
-
151
- return parentTimeGroup.crossoverMs;
152
- }
153
-
154
126
  get durationMs() {
155
127
  switch (this.mode) {
156
128
  case "fixed":
157
129
  return super.durationMs;
158
130
  case "sequence": {
159
131
  let duration = 0;
160
- for (const node of this.childTemporals) {
132
+ this.childTemporals.forEach((node, index) => {
133
+ if (index > 0) {
134
+ duration -= this.overlapMs;
135
+ }
161
136
  duration += node.durationMs;
162
- }
137
+ });
163
138
  return duration;
164
139
  }
165
140
  case "contain": {
@@ -207,9 +182,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
207
182
  this.style.display = "";
208
183
 
209
184
  const animations = this.getAnimations({ subtree: true });
185
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
186
+ this.style.setProperty(
187
+ "--ef-transition--duration",
188
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`,
189
+ );
210
190
  this.style.setProperty(
211
- "--ef-duration",
212
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
191
+ "--ef-transition-out-start",
192
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
213
193
  );
214
194
 
215
195
  for (const animation of animations) {
@@ -351,7 +351,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
351
351
  if (!this.targetElement.audioBufferTask.value) {
352
352
  return;
353
353
  }
354
- if (this.targetElement.ownCurrentTimeMs > 0) {
354
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
355
355
  const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
356
356
  const audioBufferSource = audioContext.createBufferSource();
357
357
  audioBufferSource.buffer =
@@ -364,7 +364,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
364
364
  0,
365
365
  Math.max(
366
366
  0,
367
- (this.targetElement.ownCurrentTimeMs -
367
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs -
368
368
  this.targetElement.audioBufferTask.value.startOffsetMs) /
369
369
  1000,
370
370
  ),
@@ -412,6 +412,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
412
412
  if (target instanceof EFAudio || target instanceof EFVideo) {
413
413
  return target;
414
414
  }
415
- throw new Error("Invalid target, must be an EFAudio element");
415
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
416
416
  }
417
417
  }
@@ -4,3 +4,23 @@ export const durationConverter = {
4
4
  fromAttribute: (value: string): number => parseTimeToMs(value),
5
5
  toAttribute: (value: number) => `${value}s`,
6
6
  };
7
+
8
+ const positiveDurationConverter = (error: string) => {
9
+ return {
10
+ fromAttribute: (value: string): number => {
11
+ if (value.startsWith("-")) {
12
+ throw new Error(error);
13
+ }
14
+ return parseTimeToMs(value);
15
+ },
16
+ toAttribute: (value: number) => `${value}s`,
17
+ };
18
+ };
19
+
20
+ export const trimDurationConverter = positiveDurationConverter(
21
+ "Trimstart & trimend must be a positive value in milliseconds or seconds (1s, 1000ms)",
22
+ );
23
+
24
+ export const imageDurationConverter = positiveDurationConverter(
25
+ "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
+ );
@@ -1,4 +1,5 @@
1
1
  export const parseTimeToMs = (time: string) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -5,25 +5,23 @@ import { property, state } from "lit/decorators.js";
5
5
  import { focusContext, type FocusContext } from "./focusContext.ts";
6
6
  import { focusedElementContext } from "./focusedElementContext.ts";
7
7
  import { fetchContext } from "./fetchContext.ts";
8
- import { apiHostContext } from "./apiHostContext.ts";
9
8
  import { createRef } from "lit/directives/ref.js";
10
- import { playingContext } from "./playingContext.ts";
9
+ import { loopContext, playingContext } from "./playingContext.ts";
11
10
  import type { EFTimegroup } from "../elements/EFTimegroup.ts";
11
+ import { efContext } from "./efContext.ts";
12
12
 
13
- declare class ContextMixinInterface {
14
- focusContext: FocusContext;
15
- focusedElement?: HTMLElement;
16
- fetch: typeof fetch;
13
+ export declare class ContextMixinInterface {
17
14
  signingURL?: string;
18
- apiToken?: string;
19
- apiHost: string;
20
- stageScale: number;
21
15
  rendering: boolean;
22
- stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
23
- canvasRef: ReturnType<typeof createRef<HTMLElement>>;
24
16
  playing: boolean;
25
- targetTimegroup?: EFTimegroup;
17
+ loop: boolean;
26
18
  currentTimeMs: number;
19
+ focusedElement?: HTMLElement;
20
+ stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
21
+ canvasRef: ReturnType<typeof createRef<HTMLElement>>;
22
+ targetTimegroup: EFTimegroup | null;
23
+ play(): void;
24
+ pause(): void;
27
25
  }
28
26
 
29
27
  type Constructor<T = {}> = new (...args: any[]) => T;
@@ -36,6 +34,9 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
36
34
  @state()
37
35
  focusedElement?: HTMLElement;
38
36
 
37
+ @provide({ context: efContext })
38
+ efContext = this;
39
+
39
40
  @provide({ context: fetchContext })
40
41
  fetch = async (url: string, init: RequestInit = {}) => {
41
42
  init.headers ||= {};
@@ -43,21 +44,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
43
44
  "Content-Type": "application/json",
44
45
  });
45
46
 
46
- const bearerToken = this.apiToken;
47
- if (bearerToken) {
48
- Object.assign(init.headers, {
49
- Authorization: `Bearer ${bearerToken}`,
50
- });
51
- }
52
-
53
47
  if (this.signingURL) {
54
- if (!this.#signedURLs[url]) {
55
- this.#signedURLs[url] = fetch(this.signingURL, {
48
+ if (!this.#URLTokens[url]) {
49
+ this.#URLTokens[url] = fetch(this.signingURL, {
56
50
  method: "POST",
57
51
  body: JSON.stringify({ url }),
58
52
  }).then(async (response) => {
59
53
  if (response.ok) {
60
- return (await response.json()).url;
54
+ return (await response.json()).token;
61
55
  }
62
56
  throw new Error(
63
57
  `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
@@ -65,35 +59,35 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
65
59
  });
66
60
  }
67
61
 
68
- const signedURL = await this.#signedURLs[url];
62
+ const urlToken = await this.#URLTokens[url];
69
63
 
70
- return fetch(signedURL, init);
64
+ Object.assign(init.headers, {
65
+ authorization: `Bearer ${urlToken}`,
66
+ });
71
67
  }
72
68
 
73
69
  return fetch(url, init);
74
70
  };
75
71
 
76
- #signedURLs: Record<string, Promise<string>> = {};
72
+ #URLTokens: Record<string, Promise<string>> = {};
77
73
 
74
+ /**
75
+ * A URL that will be used to generated signed tokens for accessing media files from the
76
+ * editframe API. This is used to authenticate media requests per-user.
77
+ */
78
78
  @property({ type: String })
79
79
  signingURL?: string;
80
80
 
81
- @property({ type: String })
82
- apiToken?: string;
83
-
84
- @provide({ context: apiHostContext })
85
- @property({ type: String })
86
- apiHost = "";
87
-
88
81
  @provide({ context: playingContext })
89
82
  @property({ type: Boolean, reflect: true })
90
83
  playing = false;
91
84
 
85
+ @provide({ context: loopContext })
92
86
  @property({ type: Boolean, reflect: true })
93
87
  loop = false;
94
88
 
95
89
  @state()
96
- stageScale = 1;
90
+ private stageScale = 1;
97
91
 
98
92
  @property({ type: Boolean })
99
93
  rendering = false;
@@ -108,9 +102,11 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
108
102
  if (this.isConnected && !this.rendering) {
109
103
  const canvasElement = this.canvasRef.value;
110
104
  const stageElement = this.stageRef.value;
111
- if (stageElement && canvasElement) {
105
+ const canvasChild = canvasElement?.assignedElements()[0];
106
+ if (stageElement && canvasElement && canvasChild) {
112
107
  // Determine the appropriate scale factor to make the canvas fit into
113
- // it's parent element.
108
+ canvasElement.style.width = `${canvasChild.clientWidth}px`;
109
+ canvasElement.style.height = `${canvasChild.clientHeight}px`;
114
110
  const stageWidth = stageElement.clientWidth;
115
111
  const stageHeight = stageElement.clientHeight;
116
112
  const canvasWidth = canvasElement.clientWidth;
@@ -121,12 +117,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
121
117
  const scale = stageHeight / canvasHeight;
122
118
  if (this.stageScale !== scale) {
123
119
  canvasElement.style.transform = `scale(${scale})`;
120
+ canvasElement.style.transformOrigin = "top";
124
121
  }
125
122
  this.stageScale = scale;
126
123
  } else {
127
124
  const scale = stageWidth / canvasWidth;
128
125
  if (this.stageScale !== scale) {
129
126
  canvasElement.style.transform = `scale(${scale})`;
127
+ canvasElement.style.transformOrigin = "top";
130
128
  }
131
129
  this.stageScale = scale;
132
130
  }
@@ -165,6 +163,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
165
163
  return this.querySelector("ef-timegroup");
166
164
  }
167
165
 
166
+ play() {
167
+ this.playing = true;
168
+ }
169
+
170
+ pause() {
171
+ this.playing = false;
172
+ }
173
+
168
174
  #playbackAudioContext: AudioContext | null = null;
169
175
  #playbackAnimationFrameRequest: number | null = null;
170
176
  #AUDIO_PLAYBACK_SLICE_MS = 1000;
@@ -237,7 +243,15 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
237
243
  source.onended = () => {
238
244
  bufferCount--;
239
245
  if (endMs >= toMs) {
240
- this.playing = false;
246
+ this.pause();
247
+ if (this.loop) {
248
+ this.updateComplete.then(() => {
249
+ this.currentTimeMs = 0;
250
+ this.updateComplete.then(() => {
251
+ this.play();
252
+ });
253
+ });
254
+ }
241
255
  } else {
242
256
  fillBuffer();
243
257
  }