Keep the video tab's picture following while scrubbing (#417)
Scrubbing in the video tab set currentTime on every pointer move. Each new seek aborts the running one. With a large video a seek takes longer than the gap between two moves, so none finished and the picture froze. The composition tab never had this, because its players run through the VideoLibrary, which only keeps the newest wish while a seek runs. - The pending-seek logic moves into a shared helper, src/viewport/videoSeek.ts. The VideoLibrary uses it unchanged. - The video tab seeks through the same helper and uploads the picture again after each finished seek. - Play drops a pending scrub wish. Otherwise the video would jump back right after it starts. - Scrubbing before the first picture no longer jumps back to the time the tab opened with. The video stays at the last chosen time. Tests: gate on melektaus-atx green, 303 files, 4681 tests (2 new in video-seek). e2e 66 ALL PASSED, 30 checks. Without the helper the new unit test is red ([0.1, 0.2, 0.3, 0.4, 0.5] instead of [0.1]). There is no browser check for the freeze itself, the test video seeks too fast to show it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40b6290d24
commit
c0ba2673ba
4 files changed
+181
-55
No files matched your search
@@ -40,6 +40,7 @@ import {
|
||||
zoomWave,
|
||||
} from '../sound/waveView';
|
||||
import { openVideo } from '../../viewport/VideoLibrary';
|
||||
import { chaseSeeks, seekChase, type SeekChase, seekVideo } from '../../viewport/videoSeek';
|
||||
import type { EditorState } from '../state';
|
||||
import { containRect } from '../texture/imageGrid';
|
||||
import { axisTween } from '../viewTween';
|
||||
@@ -81,6 +82,10 @@ interface Preview {
|
||||
element: HTMLVideoElement;
|
||||
/** Made once the video has shown its first frame, see `openVideo`. */
|
||||
texture: Texture | null;
|
||||
/** Its seeks while scrubbing, see `videoSeek.ts`. */
|
||||
chase: SeekChase;
|
||||
/** The moment last asked for, where it stands once it has its first frame. */
|
||||
asked: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,6 +211,8 @@ export class VideoCanvas extends Widget {
|
||||
this.playing = span;
|
||||
element.muted = false;
|
||||
element.volume = videoVolumeOf(asset.props);
|
||||
// A moment still wanted from scrubbing would send it back after the start.
|
||||
if (this.preview) this.preview.chase.wanted = null;
|
||||
element.currentTime = span.start;
|
||||
void element.play().catch(() => {});
|
||||
this.invalidate();
|
||||
@@ -339,6 +346,8 @@ export class VideoCanvas extends Widget {
|
||||
const preview: Preview = {
|
||||
file: asset.file,
|
||||
texture: null,
|
||||
chase: seekChase(),
|
||||
asked: held,
|
||||
element: openVideo(`/game-maker/data/videos/${asset.file}`, held, (texture) => {
|
||||
if (this.preview !== preview) return texture.destroy(true);
|
||||
preview.texture = texture;
|
||||
@@ -346,11 +355,20 @@ export class VideoCanvas extends Widget {
|
||||
// It played for its first frame; until Play it stands at the moment the tab shows.
|
||||
if (!this.playing) {
|
||||
preview.element.pause();
|
||||
preview.element.currentTime = held;
|
||||
seekVideo(preview.element, preview.chase, preview.asked);
|
||||
}
|
||||
this.invalidate();
|
||||
}),
|
||||
};
|
||||
chaseSeeks(
|
||||
preview.element,
|
||||
preview.chase,
|
||||
() => this.preview !== preview,
|
||||
() => {
|
||||
preview.texture?.source.update();
|
||||
this.invalidate();
|
||||
},
|
||||
);
|
||||
preview.element.addEventListener('seeked', () => this.invalidate());
|
||||
this.preview = preview;
|
||||
return preview.element;
|
||||
@@ -368,10 +386,14 @@ export class VideoCanvas extends Widget {
|
||||
preview.texture?.destroy(true);
|
||||
}
|
||||
|
||||
/** Shows a moment; while scrubbing, only the newest of the moments asked for meanwhile follows. */
|
||||
private seekTo(seconds: number): void {
|
||||
const asset = this.asset();
|
||||
const element = asset && this.player(asset);
|
||||
if (element) element.currentTime = Math.max(0, Math.min(asset.duration, seconds));
|
||||
const preview = this.preview;
|
||||
if (!element || !preview) return;
|
||||
preview.asked = Math.max(0, Math.min(asset.duration, seconds));
|
||||
seekVideo(element, preview.chase, preview.asked);
|
||||
}
|
||||
|
||||
override onKeyDown(event: KeyboardEvent): boolean {
|
||||
|
||||
Reference in new issue
Block a user