Let the video tab's Play start at the playhead and reset its button
Two faults in the video tab. A click into the ruler while a clip played stopped the playback, but the button kept showing "Stop". And Play always started at the start of the clip, wherever the playhead stood. - The tab now reports when the ruler stops the playback, and the button switches back to "Play". - Play starts at the playhead when it lies inside the clip. Without a selected clip the whole video counts. Outside or at the end, Play starts at the beginning. - So Stop and Play continue where the playback stopped. A clip that ran to its end without loop starts again at the beginning. Tests: gate on melektaus-atx green. e2e 66 ALL PASSED, 32 checks (2 new in 66e-i). With both fixes taken back, both checks are red with exactly the reported faults: the button shows "Stop", and Play runs from 0.71 s, i.e. from the clip start at 0.6 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f177ee90a4
commit
080df2d211
5 files changed
+60
-13
No files matched your search
@@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2039 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2041 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -28,6 +28,7 @@ export function videoHooks({ state, editor }: HookContext) {
|
||||
/** The middle of the clip strip, and of the track under it, at a second of the video. */
|
||||
stripAt: (seconds: number) => videoEditor.canvas.stripAt(seconds),
|
||||
trackAt: (seconds: number) => videoEditor.canvas.trackAt(seconds),
|
||||
rulerAt: (seconds: number) => videoEditor.canvas.rulerAt(seconds),
|
||||
/** The time's zoom, and whether a transition is on its way. */
|
||||
zoom: () => videoEditor.canvas.zoomInfo(),
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@ export class VideoCanvas extends Widget {
|
||||
/** The span the player is held to while it plays: the picked clip, else the whole video. */
|
||||
private playing: CutSpan | null = null;
|
||||
onTogglePlay: () => void = () => {};
|
||||
/** The player stopped by itself (the end of a clip that does not repeat). */
|
||||
/** The player stopped by itself (the end of a clip that does not repeat) or by a click in the ruler. */
|
||||
onStopped: () => void = () => {};
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
@@ -202,28 +202,43 @@ export class VideoCanvas extends Widget {
|
||||
return this.playing !== null;
|
||||
}
|
||||
|
||||
/** Plays the picked clip, else the whole video, from its start, with the video's volume and loop. */
|
||||
/**
|
||||
* Plays the picked clip, else the whole video, with the video's volume and loop: from the
|
||||
* playhead when it stands inside, else from the start.
|
||||
*/
|
||||
play(): void {
|
||||
const asset = this.asset();
|
||||
const span = this.shownSpan();
|
||||
const element = asset && this.player(asset);
|
||||
if (!asset || !span || !element) return;
|
||||
const preview = this.preview;
|
||||
if (!asset || !span || !element || !preview) return;
|
||||
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;
|
||||
preview.chase.wanted = null;
|
||||
const head = preview.asked;
|
||||
element.currentTime = head >= span.start && head < span.end - LEAST_SPAN ? head : span.start;
|
||||
void element.play().catch(() => {});
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
const preview = this.preview;
|
||||
// The playhead stays where it stopped; the next Play goes on from there.
|
||||
if (this.playing && preview) preview.asked = preview.element.currentTime;
|
||||
this.playing = null;
|
||||
this.preview?.element.pause();
|
||||
preview?.element.pause();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/** Stops from inside the canvas, and tells the header so its button follows. */
|
||||
private halt(): void {
|
||||
if (!this.isPlaying) return;
|
||||
this.stop();
|
||||
this.onStopped();
|
||||
}
|
||||
|
||||
/** Where the tab's player stands, in seconds of the video; null while nothing is shown. */
|
||||
get playhead(): number | null {
|
||||
return this.preview && this.preview.file === this.asset()?.file ? this.preview.element.currentTime : null;
|
||||
@@ -239,10 +254,7 @@ export class VideoCanvas extends Widget {
|
||||
if (videoLoopsOf(asset.props)) {
|
||||
element.currentTime = span.start;
|
||||
if (element.paused) void element.play().catch(() => {});
|
||||
} else {
|
||||
this.stop();
|
||||
this.onStopped();
|
||||
}
|
||||
} else this.halt();
|
||||
}
|
||||
this.redraw();
|
||||
}
|
||||
@@ -277,6 +289,15 @@ export class VideoCanvas extends Widget {
|
||||
};
|
||||
}
|
||||
|
||||
/** The middle of the ruler at a second of the video, on screen (test hook). */
|
||||
rulerAt(seconds: number): Vec2 {
|
||||
const origin = this.getGlobalPosition();
|
||||
return {
|
||||
x: origin.x + secondsToX(this.view, seconds),
|
||||
y: origin.y + this.timelineTop + RULER_HEIGHT / 2,
|
||||
};
|
||||
}
|
||||
|
||||
/** The middle of the track at a second of the video, on screen (test hook). */
|
||||
trackAt(seconds: number): Vec2 {
|
||||
const origin = this.getGlobalPosition();
|
||||
@@ -418,7 +439,7 @@ export class VideoCanvas extends Widget {
|
||||
const from = Math.max(0, Math.min(asset.duration, xToSeconds(this.view, p.x)));
|
||||
if (y < RULER_HEIGHT) {
|
||||
this.gesture = { kind: 'seek' };
|
||||
this.stop();
|
||||
this.halt();
|
||||
this.seekTo(from);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class VideoEditor extends AssetTabEditor<VideoAsset, VideoCanvas> {
|
||||
this.playButton = new Button('Play', {
|
||||
icon: TOOLBAR_ICON_SVG.play,
|
||||
minWidth: 72,
|
||||
tooltip: 'Play the picked clip, else the whole video (Space)',
|
||||
tooltip: 'Play the picked clip, else the whole video, from the playhead (Space)',
|
||||
onClick: () => this.togglePlay(),
|
||||
});
|
||||
this.downloadButton = new Button('Download', {
|
||||
|
||||
@@ -159,6 +159,31 @@ export async function video(t) {
|
||||
await click(playButton.x, playButton.y);
|
||||
check('Stop holds it', (await json('__gm.videoTab.playing()')) === false);
|
||||
|
||||
// 66e-i. A click in the ruler while it plays stops it, and the header's button says Play again.
|
||||
// Play then goes on from that moment, not from the clip's start.
|
||||
const tabState =
|
||||
'({ playing: __gm.videoTab.playing(), head: __gm.videoTab.playhead(), button: __gm.videoTab.playButton().text })';
|
||||
await click(playButton.x, playButton.y);
|
||||
await sleep(150);
|
||||
const ruler = await json('__gm.videoTab.rulerAt(1)');
|
||||
await click(ruler.x, ruler.y);
|
||||
await sleep(150);
|
||||
const halted = await json(tabState);
|
||||
await click(playButton.x, playButton.y);
|
||||
await sleep(150);
|
||||
const resumed = await json(tabState);
|
||||
await click(playButton.x, playButton.y);
|
||||
check(
|
||||
'a click in the ruler stops the play and its button',
|
||||
halted.playing === false && halted.button === 'Play' && Math.abs(halted.head - 1) < 0.05,
|
||||
JSON.stringify(halted),
|
||||
);
|
||||
check(
|
||||
'Play goes on from the playhead',
|
||||
resumed.playing && resumed.button === 'Stop' && resumed.head >= 1 && resumed.head < 1.4,
|
||||
JSON.stringify(resumed),
|
||||
);
|
||||
|
||||
// 66e-ii. With the clip picked (#403), a drag in the track off its handles or in the strip beside
|
||||
// its band changes nothing; one on its band moves it whole, one undo step. A click in the
|
||||
// track beside it picks the video, on it the clip again.
|
||||
|
||||
Reference in new issue
Block a user