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:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```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
|
# every tool signs in first – see the Accounts page
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.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. */
|
/** 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),
|
stripAt: (seconds: number) => videoEditor.canvas.stripAt(seconds),
|
||||||
trackAt: (seconds: number) => videoEditor.canvas.trackAt(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. */
|
/** The time's zoom, and whether a transition is on its way. */
|
||||||
zoom: () => videoEditor.canvas.zoomInfo(),
|
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. */
|
/** The span the player is held to while it plays: the picked clip, else the whole video. */
|
||||||
private playing: CutSpan | null = null;
|
private playing: CutSpan | null = null;
|
||||||
onTogglePlay: () => void = () => {};
|
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 = () => {};
|
onStopped: () => void = () => {};
|
||||||
|
|
||||||
constructor(private readonly state: EditorState) {
|
constructor(private readonly state: EditorState) {
|
||||||
@@ -202,28 +202,43 @@ export class VideoCanvas extends Widget {
|
|||||||
return this.playing !== null;
|
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 {
|
play(): void {
|
||||||
const asset = this.asset();
|
const asset = this.asset();
|
||||||
const span = this.shownSpan();
|
const span = this.shownSpan();
|
||||||
const element = asset && this.player(asset);
|
const element = asset && this.player(asset);
|
||||||
if (!asset || !span || !element) return;
|
const preview = this.preview;
|
||||||
|
if (!asset || !span || !element || !preview) return;
|
||||||
this.playing = span;
|
this.playing = span;
|
||||||
element.muted = false;
|
element.muted = false;
|
||||||
element.volume = videoVolumeOf(asset.props);
|
element.volume = videoVolumeOf(asset.props);
|
||||||
// A moment still wanted from scrubbing would send it back after the start.
|
// A moment still wanted from scrubbing would send it back after the start.
|
||||||
if (this.preview) this.preview.chase.wanted = null;
|
preview.chase.wanted = null;
|
||||||
element.currentTime = span.start;
|
const head = preview.asked;
|
||||||
|
element.currentTime = head >= span.start && head < span.end - LEAST_SPAN ? head : span.start;
|
||||||
void element.play().catch(() => {});
|
void element.play().catch(() => {});
|
||||||
this.invalidate();
|
this.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
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.playing = null;
|
||||||
this.preview?.element.pause();
|
preview?.element.pause();
|
||||||
this.invalidate();
|
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. */
|
/** Where the tab's player stands, in seconds of the video; null while nothing is shown. */
|
||||||
get playhead(): number | null {
|
get playhead(): number | null {
|
||||||
return this.preview && this.preview.file === this.asset()?.file ? this.preview.element.currentTime : 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)) {
|
if (videoLoopsOf(asset.props)) {
|
||||||
element.currentTime = span.start;
|
element.currentTime = span.start;
|
||||||
if (element.paused) void element.play().catch(() => {});
|
if (element.paused) void element.play().catch(() => {});
|
||||||
} else {
|
} else this.halt();
|
||||||
this.stop();
|
|
||||||
this.onStopped();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.redraw();
|
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). */
|
/** The middle of the track at a second of the video, on screen (test hook). */
|
||||||
trackAt(seconds: number): Vec2 {
|
trackAt(seconds: number): Vec2 {
|
||||||
const origin = this.getGlobalPosition();
|
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)));
|
const from = Math.max(0, Math.min(asset.duration, xToSeconds(this.view, p.x)));
|
||||||
if (y < RULER_HEIGHT) {
|
if (y < RULER_HEIGHT) {
|
||||||
this.gesture = { kind: 'seek' };
|
this.gesture = { kind: 'seek' };
|
||||||
this.stop();
|
this.halt();
|
||||||
this.seekTo(from);
|
this.seekTo(from);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class VideoEditor extends AssetTabEditor<VideoAsset, VideoCanvas> {
|
|||||||
this.playButton = new Button('Play', {
|
this.playButton = new Button('Play', {
|
||||||
icon: TOOLBAR_ICON_SVG.play,
|
icon: TOOLBAR_ICON_SVG.play,
|
||||||
minWidth: 72,
|
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(),
|
onClick: () => this.togglePlay(),
|
||||||
});
|
});
|
||||||
this.downloadButton = new Button('Download', {
|
this.downloadButton = new Button('Download', {
|
||||||
|
|||||||
@@ -159,6 +159,31 @@ export async function video(t) {
|
|||||||
await click(playButton.x, playButton.y);
|
await click(playButton.x, playButton.y);
|
||||||
check('Stop holds it', (await json('__gm.videoTab.playing()')) === false);
|
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
|
// 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
|
// 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.
|
// track beside it picks the video, on it the clip again.
|
||||||
|
|||||||
Reference in new issue
Block a user