summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers/playback-manager.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-13 04:41:33 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-13 05:08:29 +0200
commit7d4e166678c35d48b22b8d1d05639f981a956a8b (patch)
treeeb596d4f4609053524224169c247db24052db353 /apps/web/src/core/managers/playback-manager.ts
parentcad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149 (diff)
downloadopencut-7d4e166678c35d48b22b8d1d05639f981a956a8b.tar.gz
fix: text measurement, rotation handle, video cache races, playback sync
Made-with: Cursor
Diffstat (limited to 'apps/web/src/core/managers/playback-manager.ts')
-rw-r--r--apps/web/src/core/managers/playback-manager.ts124
1 files changed, 86 insertions, 38 deletions
diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts
index 47909bfe..341a0eae 100644
--- a/apps/web/src/core/managers/playback-manager.ts
+++ b/apps/web/src/core/managers/playback-manager.ts
@@ -13,24 +13,32 @@ export class PlaybackManager {
private playbackTimer: number | null = null;
private playbackStartWallTime = 0;
private playbackStartTime = 0;
+ private timelineScopeBound = false;
- constructor(private editor: EditorCore) {
- this.editor.timeline.subscribe(() => {
- const maxTime = this.editor.timeline.getTotalDuration();
- if (this.currentTime > maxTime && maxTime > 0) {
- this.currentTime = maxTime;
- this.notify();
- }
- });
+ constructor(private editor: EditorCore) {}
+
+ bindTimelineScope(): void {
+ if (this.timelineScopeBound) {
+ return;
+ }
+
+ const reconcile = () => {
+ this.reconcileTimelineScope();
+ };
+ this.editor.timeline.subscribe(reconcile);
+ this.editor.scenes.subscribe(reconcile);
+ this.timelineScopeBound = true;
+ this.reconcileTimelineScope();
}
play(): void {
const maxTime = this.editor.timeline.getTotalDuration();
+ if (maxTime <= 0) {
+ return;
+ }
- if (maxTime > 0) {
- if (this.currentTime >= maxTime) {
- this.seek({ time: 0 });
- }
+ if (this.currentTime >= maxTime) {
+ this.seek({ time: 0 });
}
this.isPlaying = true;
@@ -53,19 +61,13 @@ export class PlaybackManager {
}
seek({ time }: { time: number }): void {
- const maxTime = this.editor.timeline.getTotalDuration();
- this.currentTime = Math.max(0, Math.min(maxTime, time));
+ this.currentTime = this.clampTimeToTimeline(time);
if (this.isPlaying) {
this.playbackStartWallTime = performance.now();
this.playbackStartTime = this.currentTime;
}
this.notify();
-
- window.dispatchEvent(
- new CustomEvent("playback-seek", {
- detail: { time: this.currentTime },
- }),
- );
+ this.dispatchSeekEvent(this.currentTime);
}
setVolume({ volume }: { volume: number }): void {
@@ -131,6 +133,29 @@ export class PlaybackManager {
return () => this.listeners.delete(listener);
}
+ private reconcileTimelineScope(): void {
+ const maxTime = this.editor.timeline.getTotalDuration();
+ const nextTime = this.clampTimeToTimeline(this.currentTime);
+ const shouldPause = this.isPlaying && nextTime >= maxTime;
+ const timeChanged = nextTime !== this.currentTime;
+
+ if (!timeChanged && !shouldPause) {
+ return;
+ }
+
+ if (shouldPause) {
+ this.isPlaying = false;
+ this.stopTimer();
+ }
+
+ this.currentTime = nextTime;
+ this.notify();
+
+ if (timeChanged) {
+ this.dispatchSeekEvent(this.currentTime);
+ }
+ }
+
private notify(): void {
this.listeners.forEach((fn) => {
fn();
@@ -158,31 +183,54 @@ export class PlaybackManager {
if (!this.isPlaying) return;
const fps = this.editor.project.getActive()?.settings.fps;
- const elapsedSeconds = (performance.now() - this.playbackStartWallTime) / 1000;
- const rawTime = this.playbackStartTime + Math.round(elapsedSeconds * TICKS_PER_SECOND);
- const newTime = fps ? (roundToFrame({ time: rawTime, rate: fps }) ?? rawTime) : rawTime;
+ const elapsedSeconds =
+ (performance.now() - this.playbackStartWallTime) / 1000;
+ const rawTime =
+ this.playbackStartTime + Math.round(elapsedSeconds * TICKS_PER_SECOND);
+ const newTime = fps
+ ? (roundToFrame({ time: rawTime, rate: fps }) ?? rawTime)
+ : rawTime;
const maxTime = this.editor.timeline.getTotalDuration();
- if (maxTime > 0 && newTime >= maxTime) {
+ if (newTime >= maxTime) {
this.pause();
this.currentTime = maxTime;
this.notify();
-
- window.dispatchEvent(
- new CustomEvent("playback-seek", {
- detail: { time: maxTime },
- }),
- );
- } else {
- this.currentTime = newTime;
-
- window.dispatchEvent(
- new CustomEvent("playback-update", {
- detail: { time: newTime },
- }),
- );
+ this.dispatchSeekEvent(maxTime);
+ return;
}
+ this.currentTime = newTime;
+ this.dispatchUpdateEvent(newTime);
this.playbackTimer = requestAnimationFrame(this.updateTime);
};
+
+ private clampTimeToTimeline(time: number): number {
+ const maxTime = this.editor.timeline.getTotalDuration();
+ return Math.max(0, Math.min(maxTime, time));
+ }
+
+ private dispatchSeekEvent(time: number): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("playback-seek", {
+ detail: { time },
+ }),
+ );
+ }
+
+ private dispatchUpdateEvent(time: number): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("playback-update", {
+ detail: { time },
+ }),
+ );
+ }
}