@editframe/elements 0.23.7-beta.0 → 0.24.1-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.
@@ -15,8 +15,21 @@ export declare class EFTimegroup extends EFTimegroup_base {
15
15
  efContext: this;
16
16
  mode: "fit" | "fixed" | "sequence" | "contain";
17
17
  overlapMs: number;
18
+ fps: number;
18
19
  attributeChangedCallback(name: string, old: string | null, value: string | null): void;
19
20
  fit: "none" | "contain" | "cover";
21
+ /**
22
+ * Get the effective FPS for this timegroup.
23
+ * During rendering, uses the render options FPS if available.
24
+ * Otherwise uses the configured fps property.
25
+ */
26
+ get effectiveFps(): number;
27
+ /**
28
+ * Quantize a time value to the nearest frame boundary based on effectiveFps.
29
+ * @param timeSeconds - Time in seconds
30
+ * @returns Time quantized to frame boundaries in seconds
31
+ */
32
+ private quantizeToFrameTime;
20
33
  private runThrottledFrameTask;
21
34
  set currentTime(time: number);
22
35
  get currentTime(): number;
@@ -38,6 +38,7 @@ var EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
38
38
  this.efContext = this;
39
39
  this.mode = "contain";
40
40
  this.overlapMs = 0;
41
+ this.fps = 30;
41
42
  this.fit = "none";
42
43
  this.mediaDurationsPromise = void 0;
43
44
  this.frameTask = new Task(this, {
@@ -88,7 +89,8 @@ var EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
88
89
  "mode",
89
90
  "overlap",
90
91
  "currenttime",
91
- "fit"
92
+ "fit",
93
+ "fps"
92
94
  ];
93
95
  }
94
96
  static {
@@ -112,6 +114,7 @@ var EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
112
114
  attributeChangedCallback(name, old, value) {
113
115
  if (name === "mode" && value) this.mode = value;
114
116
  if (name === "overlap" && value) this.overlapMs = parseTimeToMs(value);
117
+ if (name === "fps" && value) this.fps = Number.parseFloat(value);
115
118
  super.attributeChangedCallback(name, old, value);
116
119
  }
117
120
  #resizeObserver;
@@ -119,11 +122,22 @@ var EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
119
122
  #seekInProgress = false;
120
123
  #pendingSeekTime;
121
124
  #processingPendingSeek = false;
125
+ get effectiveFps() {
126
+ if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
127
+ return this.fps;
128
+ }
129
+ quantizeToFrameTime(timeSeconds) {
130
+ const fps = this.effectiveFps;
131
+ if (!fps || fps <= 0) return timeSeconds;
132
+ const frameDurationS = 1 / fps;
133
+ return Math.round(timeSeconds / frameDurationS) * frameDurationS;
134
+ }
122
135
  async runThrottledFrameTask() {
123
136
  if (this.playbackController) return this.playbackController.runThrottledFrameTask();
124
137
  await this.frameTask.run();
125
138
  }
126
139
  set currentTime(time) {
140
+ time = this.quantizeToFrameTime(time);
127
141
  if (this.playbackController) {
128
142
  this.playbackController.currentTime = time;
129
143
  return;
@@ -409,6 +423,7 @@ var EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
409
423
  };
410
424
  __decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_timeGroupContext", void 0);
411
425
  __decorate([provide({ context: efContext })], EFTimegroup.prototype, "efContext", void 0);
426
+ __decorate([property({ type: Number })], EFTimegroup.prototype, "fps", void 0);
412
427
  __decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
413
428
  __decorate([property({
414
429
  type: Number,
@@ -27,7 +27,6 @@ var EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
27
27
  position: relative;
28
28
  }
29
29
  canvas {
30
- all: inherit;
31
30
  overflow: hidden;
32
31
  position: static;
33
32
  width: 100%;
@@ -68,7 +68,11 @@ var PlaybackController = class {
68
68
  });
69
69
  }
70
70
  get currentTime() {
71
- return this.#currentTime ?? 0;
71
+ const rawTime = this.#currentTime ?? 0;
72
+ const fps = this.#host.fps ?? 30;
73
+ if (!fps || fps <= 0) return rawTime;
74
+ const frameDurationS = 1 / fps;
75
+ return Math.round(rawTime / frameDurationS) * frameDurationS;
72
76
  }
73
77
  set currentTime(time) {
74
78
  time = Math.max(0, Math.min(this.#host.durationMs / 1e3, time));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.23.7-beta.0",
3
+ "version": "0.24.1-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "license": "UNLICENSED",
28
28
  "dependencies": {
29
29
  "@bramus/style-observer": "^1.3.0",
30
- "@editframe/assets": "0.23.7-beta.0",
30
+ "@editframe/assets": "0.24.1-beta.0",
31
31
  "@lit/context": "^1.1.6",
32
32
  "@lit/task": "^1.0.3",
33
33
  "@opentelemetry/api": "^1.9.0",
@@ -874,16 +874,16 @@ describe("EFCaptions", () => {
874
874
  const captionsTask = captions.customCaptionsDataTask;
875
875
  await captionsTask.taskComplete;
876
876
 
877
- // Test just before boundary 2.599s - should show " timing"
878
- timegroup.currentTimeMs = 2599;
877
+ // Test at frame before boundary (frame 77 at 30fps = ~2567ms) - should show " timing"
878
+ timegroup.currentTimeMs = 2567;
879
879
  await timegroup.seekTask.taskComplete;
880
880
  await captions.frameTask.taskComplete;
881
881
  await wordContainer.updateComplete;
882
882
 
883
- console.log(`At 2599ms: wordText="${wordContainer.wordText}"`);
883
+ console.log(`At 2567ms: wordText="${wordContainer.wordText}"`);
884
884
  expect(wordContainer.wordText).toBe(" timing");
885
885
 
886
- // Test at exact boundary 2.6s - should show " test" (the starting word)
886
+ // Test at exact boundary frame (frame 78 at 30fps = 2600ms) - should show " test" (the starting word)
887
887
  timegroup.currentTimeMs = 2600;
888
888
  await timegroup.seekTask.taskComplete;
889
889
  await captions.frameTask.taskComplete;
@@ -916,14 +916,14 @@ describe("EFCaptions", () => {
916
916
  const captionsTask = captions.customCaptionsDataTask;
917
917
  await captionsTask.taskComplete;
918
918
 
919
- // Test just before boundary
920
- timegroup.currentTimeMs = 2590;
919
+ // Test at frame before boundary (frame 77 at 30fps = ~2567ms)
920
+ timegroup.currentTimeMs = 2567;
921
921
  await timegroup.seekTask.taskComplete;
922
922
  await captions.frameTask.taskComplete;
923
923
  await wordContainer.updateComplete;
924
924
 
925
925
  console.log(
926
- `Demo case - At 2590ms: wordText="${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
926
+ `Demo case - At 2567ms: wordText="${wordContainer.wordText}", hidden=${wordContainer.hidden}`,
927
927
  );
928
928
  expect(wordContainer.wordText).toBe(" captions");
929
929
 
@@ -60,7 +60,14 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
60
60
  static get observedAttributes(): string[] {
61
61
  // biome-ignore lint/complexity/noThisInStatic: It's okay to use this here
62
62
  const parentAttributes = super.observedAttributes || [];
63
- return [...parentAttributes, "mode", "overlap", "currenttime", "fit"];
63
+ return [
64
+ ...parentAttributes,
65
+ "mode",
66
+ "overlap",
67
+ "currenttime",
68
+ "fit",
69
+ "fps",
70
+ ];
64
71
  }
65
72
 
66
73
  static styles = css`
@@ -89,6 +96,9 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
89
96
  mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
90
97
  overlapMs = 0;
91
98
 
99
+ @property({ type: Number })
100
+ fps = 30;
101
+
92
102
  attributeChangedCallback(
93
103
  name: string,
94
104
  old: string | null,
@@ -100,6 +110,9 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
100
110
  if (name === "overlap" && value) {
101
111
  this.overlapMs = parseTimeToMs(value);
102
112
  }
113
+ if (name === "fps" && value) {
114
+ this.fps = Number.parseFloat(value);
115
+ }
103
116
  super.attributeChangedCallback(name, old, value);
104
117
  }
105
118
 
@@ -113,6 +126,31 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
113
126
  #pendingSeekTime: number | undefined;
114
127
  #processingPendingSeek = false;
115
128
 
129
+ /**
130
+ * Get the effective FPS for this timegroup.
131
+ * During rendering, uses the render options FPS if available.
132
+ * Otherwise uses the configured fps property.
133
+ */
134
+ get effectiveFps(): number {
135
+ // During rendering, prefer the render options FPS
136
+ if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) {
137
+ return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
138
+ }
139
+ return this.fps;
140
+ }
141
+
142
+ /**
143
+ * Quantize a time value to the nearest frame boundary based on effectiveFps.
144
+ * @param timeSeconds - Time in seconds
145
+ * @returns Time quantized to frame boundaries in seconds
146
+ */
147
+ private quantizeToFrameTime(timeSeconds: number): number {
148
+ const fps = this.effectiveFps;
149
+ if (!fps || fps <= 0) return timeSeconds;
150
+ const frameDurationS = 1 / fps;
151
+ return Math.round(timeSeconds / frameDurationS) * frameDurationS;
152
+ }
153
+
116
154
  private async runThrottledFrameTask(): Promise<void> {
117
155
  if (this.playbackController) {
118
156
  return this.playbackController.runThrottledFrameTask();
@@ -122,6 +160,10 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
122
160
 
123
161
  @property({ type: Number, attribute: "currenttime" })
124
162
  set currentTime(time: number) {
163
+ // Quantize time to frame boundaries based on fps
164
+ // Do this BEFORE delegating to playbackController to ensure consistency
165
+ time = this.quantizeToFrameTime(time);
166
+
125
167
  if (this.playbackController) {
126
168
  this.playbackController.currentTime = time;
127
169
  return;
@@ -40,7 +40,6 @@ export class EFVideo extends TWMixin(EFMedia) {
40
40
  position: relative;
41
41
  }
42
42
  canvas {
43
- all: inherit;
44
43
  overflow: hidden;
45
44
  position: static;
46
45
  width: 100%;
@@ -120,7 +120,12 @@ export class PlaybackController implements ReactiveController {
120
120
  }
121
121
 
122
122
  get currentTime(): number {
123
- return this.#currentTime ?? 0;
123
+ const rawTime = this.#currentTime ?? 0;
124
+ // Quantize to frame boundaries based on host's fps
125
+ const fps = (this.#host as any).fps ?? 30;
126
+ if (!fps || fps <= 0) return rawTime;
127
+ const frameDurationS = 1 / fps;
128
+ return Math.round(rawTime / frameDurationS) * frameDurationS;
124
129
  }
125
130
 
126
131
  set currentTime(time: number) {