Show a cut into the middle of a clip without its old picture (#404)
A switched-off driven video waits at the video time it has where it is, before its first key mostly the start value. Switched on at another spot, its player has to seek first. Until the seek is done the texture still holds the old picture, so the start of the clip flashed for a frame or two at the cut. - A view holds its last picture while a driven video that just came into sight is still seeking, for at most 0.3 s. At a cut the old clip stays one frame longer and the right picture of the new one follows. - A seek counts as done only when the browser hands in the next video frame (requestVideoFrameCallback), and the texture is uploaded right then. The seeked event alone was too early, one red frame was left. - A seek that takes longer than 0.3 s leaves the sprite out until its picture is there. The wrong picture does not show either way. Tests: - e2e 66q switches a sprite off at 0.2 s (red) and on at 1.2 s (blue) and reads every frame in the browser. No red may show, blue must stand at the end. - Against the old code 66q fails with 2 red frames before the blue. With only the seeked event it still fails with 1 red frame. - e2e 66 on melektaus-atx: ALL PASSED, 28 checks, run twice. - Unit video-library, video-follow: 19 passed. - Gate on melektaus-atx: CHECK EXIT 0, 297 files, 4562 tests. - tsc, eslint, Prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a1e420806
commit
b9fcf48097
4 files changed
+145
-19
No files matched your search
@@ -57,6 +57,8 @@ interface Drive {
|
||||
reading: SpeedReading | null;
|
||||
/** The newest seek asked for while one was still running: only that one follows. */
|
||||
wanted: number | null;
|
||||
/** Set from a seek until the picture it went to is in the texture, see `seek`. */
|
||||
landing: boolean;
|
||||
}
|
||||
|
||||
interface Player {
|
||||
@@ -130,6 +132,15 @@ export class VideoLibrary {
|
||||
return this.players.get(key)?.texture != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player behind `key` shows the picture its wearer asked for last: it has one and no
|
||||
* seek is under way. Until a seek has landed, its texture still holds the picture it left.
|
||||
*/
|
||||
showsAsked(key: string): boolean {
|
||||
const player = this.players.get(key);
|
||||
return player?.texture != null && !player.drive?.landing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a driven player (#404) the view `view` wears (see `wear`) is still without its first
|
||||
* picture. A run waits for them at its start: the Game tab opens players of its own at Play.
|
||||
@@ -180,7 +191,7 @@ export class VideoLibrary {
|
||||
// A video that ran to the end of its file starts over from its first picture on play().
|
||||
end: element.ended ? element.currentTime : Math.min(play.end, element.duration),
|
||||
});
|
||||
if (step.seek !== null) this.seek(element, drive, step.seek);
|
||||
if (step.seek !== null) this.seek(player, drive, step.seek);
|
||||
if (step.kind === 'hold') {
|
||||
element.muted = true;
|
||||
if (!element.paused) element.pause();
|
||||
@@ -192,10 +203,28 @@ export class VideoLibrary {
|
||||
if (element.paused) void element.play().catch(() => {});
|
||||
}
|
||||
|
||||
/** Seeks, or, while a seek is still running, remembers the newest wish for when it is done. */
|
||||
private seek(element: HTMLVideoElement, drive: Drive, time: number): void {
|
||||
/**
|
||||
* Seeks, or, while a seek is still running, remembers the newest wish for when it is done. The
|
||||
* seek has landed with the first frame Chrome hands over once all of them are done: at `seeked`
|
||||
* the picture was not there yet, and Pixi, which loads a paused video's picture only then, kept
|
||||
* the one before it for a frame.
|
||||
*/
|
||||
private seek(player: Player, drive: Drive, time: number): void {
|
||||
const { element } = player;
|
||||
if (element.seeking) drive.wanted = time;
|
||||
else element.currentTime = time;
|
||||
if (drive.landing) return;
|
||||
drive.landing = true;
|
||||
const land = () => {
|
||||
if (player.gone) return;
|
||||
if (element.seeking || drive.wanted !== null) {
|
||||
element.requestVideoFrameCallback(land);
|
||||
return;
|
||||
}
|
||||
player.texture?.source.update();
|
||||
drive.landing = false;
|
||||
};
|
||||
element.requestVideoFrameCallback(land);
|
||||
}
|
||||
|
||||
/** Lets every player go: a session that ends takes its videos along. */
|
||||
@@ -255,7 +284,7 @@ export class VideoLibrary {
|
||||
const play = parseVideoKey(key);
|
||||
if (!play) return null;
|
||||
const drive: Drive | null = isDrivenVideoKey(key)
|
||||
? { time, heard: false, reading: null, wanted: null }
|
||||
? { time, heard: false, reading: null, wanted: null, landing: false }
|
||||
: null;
|
||||
const at = drive ? drivenTimeOf(play, time, NaN) : play.start;
|
||||
const player: Player = {
|
||||
|
||||
Reference in new issue
Block a user