fix(core): optimistic current time update on seek to prevent slider snap-back (#799)

This commit is contained in:
rahim
2026-03-09 19:45:00 -07:00
committed by GitHub
parent e417ec3419
commit c605df50f6
2 changed files with 52 additions and 1 deletions
@@ -151,6 +151,51 @@ describe('timeFeature', () => {
const result2 = await seek2Promise;
expect(result2).toBe(20);
});
it('optimistically updates currentTime before seeked event fires', () => {
const video = createMockVideo({ readyState: HTMLMediaElement.HAVE_METADATA });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.currentTime).toBe(0);
// Start seek but don't fire any DOM events.
store.seek(45);
// Store should reflect target time immediately (no waiting for seeked).
expect(store.state.currentTime).toBe(45);
});
it('optimistically sets seeking to true before seeking event fires', () => {
const video = createMockVideo({ readyState: HTMLMediaElement.HAVE_METADATA });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
expect(store.state.seeking).toBe(false);
store.seek(45);
expect(store.state.seeking).toBe(true);
});
it('optimistic seeking is corrected by seeked event', async () => {
const video = createMockVideo({ readyState: HTMLMediaElement.HAVE_METADATA });
const store = createStore<PlayerTarget>()(timeFeature);
store.attach({ media: video, container: null });
const resultPromise = store.seek(45);
expect(store.state.seeking).toBe(true);
// Simulate browser completing seek.
Object.defineProperty(video, 'seeking', { value: false, configurable: true });
video.dispatchEvent(new Event('seeked'));
await resultPromise;
expect(store.state.seeking).toBe(false);
expect(store.state.currentTime).toBe(45);
});
});
});
});
+7 -1
View File
@@ -7,7 +7,7 @@ import { signalKeys } from '../signal-keys';
export const timeFeature = definePlayerFeature({
name: 'time',
state: ({ target, signals }): MediaTimeState => ({
state: ({ target, signals, set }): MediaTimeState => ({
currentTime: 0,
duration: 0,
seeking: false,
@@ -23,6 +23,12 @@ export const timeFeature = definePlayerFeature({
// Perform the seek and wait for it to complete.
const clampedTime = Math.max(0, Math.min(time, media.duration || Infinity));
// Optimistic update: reflect the target position immediately so UI consumers
// (e.g. time slider) don't snap back to the old currentTime while waiting
// for the browser's async seeking/seeked events.
set({ currentTime: clampedTime, seeking: true });
media.currentTime = clampedTime;
await onEvent(media, 'seeked', { signal }).catch(noop);