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
+2
@@ -338,6 +338,8 @@ limitations under the License.
|
|||||||
playsinline
|
playsinline
|
||||||
(loadedmetadata)="onLoadedMetadata()"
|
(loadedmetadata)="onLoadedMetadata()"
|
||||||
(timeupdate)="onTimeUpdate()"
|
(timeupdate)="onTimeUpdate()"
|
||||||
|
(seeked)="onSeeked()"
|
||||||
|
(emptied)="onPause()"
|
||||||
(play)="onPlay()"
|
(play)="onPlay()"
|
||||||
(pause)="onPause()"
|
(pause)="onPause()"
|
||||||
(ended)="onEnded()"
|
(ended)="onEnded()"
|
||||||
|
|||||||
+202
-5
@@ -1,17 +1,26 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { signal } from '@angular/core';
|
import { signal } from '@angular/core';
|
||||||
import { AgentService } from '../../services/agent.service';
|
import { AgentService } from '../../services/agent.service';
|
||||||
import { StepReplayFrame } from '../../core/models/stream.model';
|
import { StepReplayFrame } from '../../core/models/stream.model';
|
||||||
import { FloatingVideoPlayerComponent } from './floating-video-player.component';
|
import { FloatingVideoPlayerComponent } from './floating-video-player.component';
|
||||||
|
|
||||||
describe('FloatingVideoPlayerComponent video markers', () => {
|
describe('FloatingVideoPlayerComponent video markers', () => {
|
||||||
it('shows a recorded step on the video timeline and seeks when clicked', () => {
|
let fixture: ComponentFixture<FloatingVideoPlayerComponent>;
|
||||||
|
let component: FloatingVideoPlayerComponent;
|
||||||
|
let video: HTMLVideoElement;
|
||||||
|
let paused: boolean;
|
||||||
|
let seeking: boolean;
|
||||||
|
let time: number;
|
||||||
|
let play: jasmine.Spy;
|
||||||
|
let agentService: ReturnType<typeof createAgentService>;
|
||||||
|
|
||||||
|
function createAgentService() {
|
||||||
const start = 1_800_000_000;
|
const start = 1_800_000_000;
|
||||||
const frame: StepReplayFrame = {
|
const frame: StepReplayFrame = {
|
||||||
index: 0, stepNumber: 1, title: '步骤 1: 点击设置', imageUrl: 'step.png',
|
index: 0, stepNumber: 1, title: '步骤 1: 点击设置', imageUrl: 'step.png',
|
||||||
actionText: '点击设置', timestamp: start + 5
|
actionText: '点击设置', timestamp: start + 5
|
||||||
};
|
};
|
||||||
const agentService = {
|
return {
|
||||||
isVideoWindowOpen: signal(true),
|
isVideoWindowOpen: signal(true),
|
||||||
isVideoMinimized: signal(false),
|
isVideoMinimized: signal(false),
|
||||||
recordingPlaybackStatus: signal('ready'),
|
recordingPlaybackStatus: signal('ready'),
|
||||||
@@ -25,15 +34,57 @@ describe('FloatingVideoPlayerComponent video markers', () => {
|
|||||||
currentSession: signal({ session_id: 'test', start_time: start }),
|
currentSession: signal({ session_id: 'test', start_time: start }),
|
||||||
currentSessionId: signal('test'),
|
currentSessionId: signal('test'),
|
||||||
videoSeekRequest: signal(null),
|
videoSeekRequest: signal(null),
|
||||||
stepSeekRequest: signal(null)
|
stepSeekRequest: signal(null),
|
||||||
|
consumeVideoAutoplay: jasmine.createSpy('consumeVideoAutoplay').and.returnValue(false)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
agentService = createAgentService();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [FloatingVideoPlayerComponent],
|
imports: [FloatingVideoPlayerComponent],
|
||||||
providers: [{ provide: AgentService, useValue: agentService }]
|
providers: [{ provide: AgentService, useValue: agentService }]
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(FloatingVideoPlayerComponent);
|
fixture = TestBed.createComponent(FloatingVideoPlayerComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
video = component.videoRef!.nativeElement;
|
||||||
|
paused = true;
|
||||||
|
seeking = false;
|
||||||
|
time = 0;
|
||||||
|
Object.defineProperties(video, {
|
||||||
|
paused: { configurable: true, get: () => paused },
|
||||||
|
seeking: { configurable: true, get: () => seeking },
|
||||||
|
ended: { configurable: true, get: () => false },
|
||||||
|
duration: { configurable: true, get: () => 20 },
|
||||||
|
currentTime: {
|
||||||
|
configurable: true, get: () => time,
|
||||||
|
set: (value: number) => { time = value; seeking = true; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spyOn(video, 'pause').and.callFake(() => {
|
||||||
|
paused = true;
|
||||||
|
video.dispatchEvent(new Event('pause'));
|
||||||
|
});
|
||||||
|
play = spyOn(video, 'play').and.callFake(() => {
|
||||||
|
paused = false;
|
||||||
|
video.dispatchEvent(new Event('play'));
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function finishSeek(): void {
|
||||||
|
seeking = false;
|
||||||
|
video.dispatchEvent(new Event('seeked'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playButton(): HTMLButtonElement {
|
||||||
|
return fixture.nativeElement.querySelector('.play-btn');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('shows a recorded step on the video timeline and seeks when clicked', () => {
|
||||||
const marker = fixture.nativeElement.querySelector('.video-marker') as HTMLButtonElement;
|
const marker = fixture.nativeElement.querySelector('.video-marker') as HTMLButtonElement;
|
||||||
expect(marker).not.toBeNull();
|
expect(marker).not.toBeNull();
|
||||||
expect(marker.getAttribute('aria-label')).toContain('步骤 1');
|
expect(marker.getAttribute('aria-label')).toContain('步骤 1');
|
||||||
@@ -43,4 +94,150 @@ describe('FloatingVideoPlayerComponent video markers', () => {
|
|||||||
marker.click();
|
marker.click();
|
||||||
expect(seek).toHaveBeenCalledOnceWith(5);
|
expect(seek).toHaveBeenCalledOnceWith(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resumes a playing video after a marker seek finishes and synchronizes the button', async () => {
|
||||||
|
component.togglePlay();
|
||||||
|
await Promise.resolve();
|
||||||
|
play.calls.reset();
|
||||||
|
fixture.nativeElement.querySelector('.video-marker').click();
|
||||||
|
expect(time).toBe(5);
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
finishSeek();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(play).toHaveBeenCalledTimes(1);
|
||||||
|
expect(playButton().title).toBe('暂停');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a paused video paused after a marker seek and resumes with one click', async () => {
|
||||||
|
fixture.nativeElement.querySelector('.video-marker').click();
|
||||||
|
finishSeek();
|
||||||
|
expect(paused).toBeTrue();
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
expect(playButton().title).toBe('播放');
|
||||||
|
playButton().click();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(playButton().title).toBe('暂停');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves playing intent across rapid consecutive marker seeks', () => {
|
||||||
|
component.togglePlay();
|
||||||
|
component.seek(5);
|
||||||
|
component.seek(8);
|
||||||
|
play.calls.reset();
|
||||||
|
finishSeek();
|
||||||
|
expect(time).toBe(8);
|
||||||
|
expect(play).toHaveBeenCalledTimes(1);
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors play and pause clicks while the destination is still seeking', () => {
|
||||||
|
component.togglePlay();
|
||||||
|
component.seek(5);
|
||||||
|
play.calls.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(playButton().title).toBe('播放');
|
||||||
|
playButton().click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(playButton().title).toBe('暂停');
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
playButton().click();
|
||||||
|
finishSeek();
|
||||||
|
expect(paused).toBeTrue();
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
expect(playButton().title).toBe('播放');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('waits for the destination segment to seek before resuming playback', () => {
|
||||||
|
agentService.activeVideoSegments.set([
|
||||||
|
{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 },
|
||||||
|
{ url: '/second.mp4', start: 20, duration: 20, width: 400, height: 800 }
|
||||||
|
]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.togglePlay();
|
||||||
|
component.seek(25);
|
||||||
|
play.calls.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
video.dispatchEvent(new Event('loadedmetadata'));
|
||||||
|
expect(component.activeSegmentIndex()).toBe(1);
|
||||||
|
expect(time).toBe(5);
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
finishSeek();
|
||||||
|
expect(play).toHaveBeenCalledTimes(1);
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(component.currentTime()).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a paused video paused when jumping to another segment', () => {
|
||||||
|
agentService.activeVideoSegments.set([
|
||||||
|
{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 },
|
||||||
|
{ url: '/second.mp4', start: 20, duration: 20, width: 400, height: 800 }
|
||||||
|
]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.seek(25);
|
||||||
|
fixture.detectChanges();
|
||||||
|
video.dispatchEvent(new Event('loadedmetadata'));
|
||||||
|
finishSeek();
|
||||||
|
expect(time).toBe(5);
|
||||||
|
expect(paused).toBeTrue();
|
||||||
|
expect(play).not.toHaveBeenCalled();
|
||||||
|
expect(playButton().title).toBe('播放');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resumes after seeking a recording without a segment manifest', () => {
|
||||||
|
agentService.activeVideoSegments.set([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
video.dispatchEvent(new Event('loadedmetadata'));
|
||||||
|
component.togglePlay();
|
||||||
|
component.seek(5);
|
||||||
|
play.calls.reset();
|
||||||
|
finishSeek();
|
||||||
|
expect(time).toBe(5);
|
||||||
|
expect(component.currentTime()).toBe(5);
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(play).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets a rejected play request so a single subsequent click can resume', async () => {
|
||||||
|
play.and.callFake(() => {
|
||||||
|
paused = false;
|
||||||
|
video.dispatchEvent(new Event('play'));
|
||||||
|
return Promise.reject(new DOMException('Interrupted', 'AbortError'));
|
||||||
|
});
|
||||||
|
component.seek(5, true);
|
||||||
|
finishSeek();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(paused).toBeTrue();
|
||||||
|
expect(playButton().title).toBe('播放');
|
||||||
|
play.and.callFake(() => {
|
||||||
|
paused = false;
|
||||||
|
video.dispatchEvent(new Event('play'));
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
playButton().click();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(playButton().title).toBe('暂停');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an older aborted play request after a newer seek resumes', async () => {
|
||||||
|
let rejectOldPlay!: (reason: unknown) => void;
|
||||||
|
play.and.returnValue(new Promise<void>((_, reject) => { rejectOldPlay = reject; }));
|
||||||
|
component.togglePlay();
|
||||||
|
paused = false;
|
||||||
|
component.seek(5);
|
||||||
|
play.and.callFake(() => { paused = false; return Promise.resolve(); });
|
||||||
|
finishSeek();
|
||||||
|
rejectOldPlay(new DOMException('Interrupted', 'AbortError'));
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(paused).toBeFalse();
|
||||||
|
expect(component.isPlaying()).toBeTrue();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+93
-28
@@ -71,6 +71,8 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
private pendingLocalTime: number | null = null;
|
private pendingLocalTime: number | null = null;
|
||||||
private pendingAutoplay = false;
|
private pendingAutoplay = false;
|
||||||
private pendingAbsoluteSeek: number | null = null;
|
private pendingAbsoluteSeek: number | null = null;
|
||||||
|
private seekPlaybackIntent: boolean | null = null;
|
||||||
|
private playRequestId = 0;
|
||||||
|
|
||||||
// Step Replay state signals
|
// Step Replay state signals
|
||||||
public activeStepIndex = signal<number>(0);
|
public activeStepIndex = signal<number>(0);
|
||||||
@@ -138,6 +140,10 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
this.currentTime.set(0);
|
this.currentTime.set(0);
|
||||||
this.duration.set(segments.reduce((sum, segment) => sum + segment.duration, 0));
|
this.duration.set(segments.reduce((sum, segment) => sum + segment.duration, 0));
|
||||||
this.isPlaying.set(false);
|
this.isPlaying.set(false);
|
||||||
|
this.pendingLocalTime = null;
|
||||||
|
this.pendingAutoplay = false;
|
||||||
|
this.seekPlaybackIntent = null;
|
||||||
|
this.playRequestId++;
|
||||||
},
|
},
|
||||||
{ allowSignalWrites: true }
|
{ allowSignalWrites: true }
|
||||||
);
|
);
|
||||||
@@ -315,20 +321,19 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
// that segment's metadata event apply pendingLocalTime.
|
// that segment's metadata event apply pendingLocalTime.
|
||||||
if (this.pendingLocalTime !== null) return;
|
if (this.pendingLocalTime !== null) return;
|
||||||
}
|
}
|
||||||
if (this.pendingLocalTime !== null) {
|
const autoplay = this.agentService.consumeVideoAutoplay();
|
||||||
v.currentTime = Math.max(0, Math.min(v.duration || this.pendingLocalTime, this.pendingLocalTime));
|
if (autoplay) {
|
||||||
this.pendingLocalTime = null;
|
|
||||||
}
|
|
||||||
if (this.pendingAutoplay) {
|
|
||||||
this.pendingAutoplay = false;
|
|
||||||
v.play().catch(() => {});
|
|
||||||
}
|
|
||||||
if (this.agentService.consumeVideoAutoplay()) {
|
|
||||||
this.isMuted.set(true);
|
this.isMuted.set(true);
|
||||||
v.muted = 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 {
|
public onPlay(): void {
|
||||||
this.isPlaying.set(true);
|
this.isPlaying.set(!this.videoRef?.nativeElement.paused);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onPause(): void {
|
public onPause(): void {
|
||||||
this.isPlaying.set(false);
|
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 {
|
public onEnded(): void {
|
||||||
|
this.isPlaying.set(false);
|
||||||
const segments = this.agentService.activeVideoSegments();
|
const segments = this.agentService.activeVideoSegments();
|
||||||
if (segments.length && this.activeSegmentIndex() < segments.length - 1) {
|
if (segments.length && this.activeSegmentIndex() < segments.length - 1) {
|
||||||
this.pendingLocalTime = 0;
|
this.pendingLocalTime = 0;
|
||||||
@@ -369,6 +388,10 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onError(): void {
|
public onError(): void {
|
||||||
|
this.playRequestId++;
|
||||||
|
this.pendingLocalTime = null;
|
||||||
|
this.pendingAutoplay = false;
|
||||||
|
this.seekPlaybackIntent = null;
|
||||||
this.videoLoadError.set(true);
|
this.videoLoadError.set(true);
|
||||||
this.isPlaying.set(false);
|
this.isPlaying.set(false);
|
||||||
}
|
}
|
||||||
@@ -379,11 +402,52 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
public togglePlay(): void {
|
public togglePlay(): void {
|
||||||
const v = this.videoRef?.nativeElement;
|
const v = this.videoRef?.nativeElement;
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
if (v.paused) {
|
if (this.pendingLocalTime !== null) {
|
||||||
v.play().catch(() => {});
|
this.pendingAutoplay = !this.isPlaying();
|
||||||
} else {
|
this.isPlaying.set(this.pendingAutoplay);
|
||||||
v.pause();
|
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 {
|
public restart(): void {
|
||||||
@@ -421,19 +485,19 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
const target = Math.max(0, Math.min(this.duration(), seconds));
|
const target = Math.max(0, Math.min(this.duration(), seconds));
|
||||||
const segments = this.agentService.activeVideoSegments();
|
const segments = this.agentService.activeVideoSegments();
|
||||||
const location = locateTimelineTime(segments, target);
|
const location = locateTimelineTime(segments, target);
|
||||||
|
const shouldPlay = autoplay || this.pendingAutoplay || (this.seekPlaybackIntent ?? !v.paused);
|
||||||
if (!location) {
|
if (!location) {
|
||||||
v.currentTime = target;
|
this.seekWithinVideo(target, shouldPlay);
|
||||||
if (autoplay) v.play().catch(() => {});
|
} else if (location.index === this.activeSegmentIndex() && this.pendingLocalTime === null) {
|
||||||
return;
|
this.seekWithinVideo(location.localTime, shouldPlay);
|
||||||
}
|
|
||||||
const { index, localTime } = location;
|
|
||||||
if (index === this.activeSegmentIndex()) {
|
|
||||||
v.currentTime = localTime;
|
|
||||||
if (autoplay) v.play().catch(() => {});
|
|
||||||
} else {
|
} else {
|
||||||
this.pendingLocalTime = localTime;
|
this.playRequestId++;
|
||||||
this.pendingAutoplay = autoplay || !v.paused;
|
this.seekPlaybackIntent = null;
|
||||||
this.activeSegmentIndex.set(index);
|
v.pause();
|
||||||
|
this.isPlaying.set(false);
|
||||||
|
this.pendingLocalTime = location.localTime;
|
||||||
|
this.pendingAutoplay = shouldPlay;
|
||||||
|
this.activeSegmentIndex.set(location.index);
|
||||||
}
|
}
|
||||||
this.currentTime.set(target);
|
this.currentTime.set(target);
|
||||||
}
|
}
|
||||||
@@ -641,6 +705,7 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void {
|
public ngOnDestroy(): void {
|
||||||
|
this.playRequestId++;
|
||||||
if (this.stepTimer) {
|
if (this.stepTimer) {
|
||||||
clearTimeout(this.stepTimer);
|
clearTimeout(this.stepTimer);
|
||||||
this.stepTimer = null;
|
this.stepTimer = null;
|
||||||
|
|||||||
Reference in new issue
Block a user