fix: preserve playback state when seeking video steps
This commit is contained in:
1 parent
1fdf7c0d1d
commit
079d7b1e28
3 files changed
+297
-33
No files matched your search
+93
-28
@@ -71,6 +71,8 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
private pendingLocalTime: number | null = null;
|
||||
private pendingAutoplay = false;
|
||||
private pendingAbsoluteSeek: number | null = null;
|
||||
private seekPlaybackIntent: boolean | null = null;
|
||||
private playRequestId = 0;
|
||||
|
||||
// Step Replay state signals
|
||||
public activeStepIndex = signal<number>(0);
|
||||
@@ -138,6 +140,10 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
this.currentTime.set(0);
|
||||
this.duration.set(segments.reduce((sum, segment) => sum + segment.duration, 0));
|
||||
this.isPlaying.set(false);
|
||||
this.pendingLocalTime = null;
|
||||
this.pendingAutoplay = false;
|
||||
this.seekPlaybackIntent = null;
|
||||
this.playRequestId++;
|
||||
},
|
||||
{ allowSignalWrites: true }
|
||||
);
|
||||
@@ -315,20 +321,19 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
// that segment's metadata event apply pendingLocalTime.
|
||||
if (this.pendingLocalTime !== null) return;
|
||||
}
|
||||
if (this.pendingLocalTime !== null) {
|
||||
v.currentTime = Math.max(0, Math.min(v.duration || this.pendingLocalTime, this.pendingLocalTime));
|
||||
this.pendingLocalTime = null;
|
||||
}
|
||||
if (this.pendingAutoplay) {
|
||||
this.pendingAutoplay = false;
|
||||
v.play().catch(() => {});
|
||||
}
|
||||
if (this.agentService.consumeVideoAutoplay()) {
|
||||
const autoplay = this.agentService.consumeVideoAutoplay();
|
||||
if (autoplay) {
|
||||
this.isMuted.set(true);
|
||||
v.muted = true;
|
||||
v.play().catch(() => {
|
||||
this.isPlaying.set(false);
|
||||
});
|
||||
}
|
||||
const shouldPlay = this.pendingAutoplay || autoplay;
|
||||
this.pendingAutoplay = false;
|
||||
if (this.pendingLocalTime !== null) {
|
||||
const localTime = Math.max(0, Math.min(v.duration || this.pendingLocalTime, this.pendingLocalTime));
|
||||
this.pendingLocalTime = null;
|
||||
this.seekWithinVideo(localTime, shouldPlay);
|
||||
} else if (shouldPlay) {
|
||||
this.playVideo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,14 +351,28 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public onPlay(): void {
|
||||
this.isPlaying.set(true);
|
||||
this.isPlaying.set(!this.videoRef?.nativeElement.paused);
|
||||
}
|
||||
|
||||
public onPause(): void {
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
|
||||
public onSeeked(): void {
|
||||
const v = this.videoRef?.nativeElement;
|
||||
if (!v || v.seeking || this.pendingLocalTime !== null) return;
|
||||
const shouldPlay = this.seekPlaybackIntent;
|
||||
this.seekPlaybackIntent = null;
|
||||
this.onTimeUpdate();
|
||||
if (shouldPlay) {
|
||||
this.playVideo();
|
||||
} else {
|
||||
this.isPlaying.set(!v.paused && !v.ended);
|
||||
}
|
||||
}
|
||||
|
||||
public onEnded(): void {
|
||||
this.isPlaying.set(false);
|
||||
const segments = this.agentService.activeVideoSegments();
|
||||
if (segments.length && this.activeSegmentIndex() < segments.length - 1) {
|
||||
this.pendingLocalTime = 0;
|
||||
@@ -369,6 +388,10 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public onError(): void {
|
||||
this.playRequestId++;
|
||||
this.pendingLocalTime = null;
|
||||
this.pendingAutoplay = false;
|
||||
this.seekPlaybackIntent = null;
|
||||
this.videoLoadError.set(true);
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
@@ -379,11 +402,52 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
public togglePlay(): void {
|
||||
const v = this.videoRef?.nativeElement;
|
||||
if (!v) return;
|
||||
if (v.paused) {
|
||||
v.play().catch(() => {});
|
||||
} else {
|
||||
v.pause();
|
||||
if (this.pendingLocalTime !== null) {
|
||||
this.pendingAutoplay = !this.isPlaying();
|
||||
this.isPlaying.set(this.pendingAutoplay);
|
||||
return;
|
||||
}
|
||||
if (this.seekPlaybackIntent !== null) {
|
||||
this.seekPlaybackIntent = !this.isPlaying();
|
||||
this.isPlaying.set(this.seekPlaybackIntent);
|
||||
return;
|
||||
}
|
||||
if (v.paused) {
|
||||
this.playVideo();
|
||||
} else {
|
||||
this.playRequestId++;
|
||||
v.pause();
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private playVideo(): void {
|
||||
const v = this.videoRef?.nativeElement;
|
||||
if (!v) return;
|
||||
const requestId = ++this.playRequestId;
|
||||
v.play().then(() => {
|
||||
if (requestId === this.playRequestId) this.isPlaying.set(!v.paused && !v.ended);
|
||||
}).catch(() => {
|
||||
// A rejected play request can leave paused=false without advancing frames.
|
||||
// Reset the native state too, so the next click really starts playback.
|
||||
if (requestId === this.playRequestId) {
|
||||
v.pause();
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private seekWithinVideo(seconds: number, shouldPlay: boolean): void {
|
||||
const v = this.videoRef?.nativeElement;
|
||||
if (!v) return;
|
||||
this.playRequestId++;
|
||||
this.seekPlaybackIntent = shouldPlay;
|
||||
// Cancel any unfinished play request before seeking; resume only once the
|
||||
// browser has decoded the destination, including after a source switch.
|
||||
v.pause();
|
||||
this.isPlaying.set(false);
|
||||
v.currentTime = seconds;
|
||||
if (!v.seeking) this.onSeeked();
|
||||
}
|
||||
|
||||
public restart(): void {
|
||||
@@ -421,19 +485,19 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
const target = Math.max(0, Math.min(this.duration(), seconds));
|
||||
const segments = this.agentService.activeVideoSegments();
|
||||
const location = locateTimelineTime(segments, target);
|
||||
const shouldPlay = autoplay || this.pendingAutoplay || (this.seekPlaybackIntent ?? !v.paused);
|
||||
if (!location) {
|
||||
v.currentTime = target;
|
||||
if (autoplay) v.play().catch(() => {});
|
||||
return;
|
||||
}
|
||||
const { index, localTime } = location;
|
||||
if (index === this.activeSegmentIndex()) {
|
||||
v.currentTime = localTime;
|
||||
if (autoplay) v.play().catch(() => {});
|
||||
this.seekWithinVideo(target, shouldPlay);
|
||||
} else if (location.index === this.activeSegmentIndex() && this.pendingLocalTime === null) {
|
||||
this.seekWithinVideo(location.localTime, shouldPlay);
|
||||
} else {
|
||||
this.pendingLocalTime = localTime;
|
||||
this.pendingAutoplay = autoplay || !v.paused;
|
||||
this.activeSegmentIndex.set(index);
|
||||
this.playRequestId++;
|
||||
this.seekPlaybackIntent = null;
|
||||
v.pause();
|
||||
this.isPlaying.set(false);
|
||||
this.pendingLocalTime = location.localTime;
|
||||
this.pendingAutoplay = shouldPlay;
|
||||
this.activeSegmentIndex.set(location.index);
|
||||
}
|
||||
this.currentTime.set(target);
|
||||
}
|
||||
@@ -641,6 +705,7 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.playRequestId++;
|
||||
if (this.stepTimer) {
|
||||
clearTimeout(this.stepTimer);
|
||||
this.stepTimer = null;
|
||||
|
||||
Reference in new issue
Block a user