fix: adjustShuttle reads live rate with 0.25 step, fullscreen targets documentElement

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 19:45:50 -04:00
parent 980f31a414
commit c00c215d33
3 changed files with 72 additions and 13 deletions

View File

@@ -262,7 +262,7 @@
case 'j': case 'j':
case 'J': case 'J':
e.preventDefault(); e.preventDefault();
shuttleRate = adjustShuttle(-1, shuttleRate); shuttleRate = adjustShuttle(-1);
break; break;
case 'k': case 'k':
case 'K': case 'K':
@@ -272,7 +272,7 @@
case 'l': case 'l':
case 'L': case 'L':
e.preventDefault(); e.preventDefault();
shuttleRate = adjustShuttle(1, shuttleRate); shuttleRate = adjustShuttle(1);
break; break;
case 'Delete': case 'Delete':
case 'Backspace': { case 'Backspace': {

View File

@@ -84,11 +84,12 @@ export function resetShuttleRate() {
} }
} }
export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number { export function adjustShuttle(dir: 1 | -1): number {
const videoEl = getVideo(); const videoEl = getVideo();
if (!videoEl) return shuttleRate; if (!videoEl) return 1;
const nextRate = Math.max(0.25, Math.min(4, shuttleRate + dir * 0.5)); const current = videoEl.playbackRate;
const nextRate = Math.max(0.25, Math.min(4, current + dir * 0.25));
videoEl.playbackRate = nextRate; videoEl.playbackRate = nextRate;
if (videoEl.paused) { if (videoEl.paused) {
void videoEl.play(); void videoEl.play();
@@ -121,14 +122,13 @@ export function toggleFullscreen(): void {
console.error('Exit fullscreen failed:', e); console.error('Exit fullscreen failed:', e);
}); });
} else { } else {
const videoEl = getVideo(); const el = document.documentElement;
if (!videoEl) return; if (el.requestFullscreen) {
if (videoEl.requestFullscreen) { el.requestFullscreen().catch((e) => {
videoEl.requestFullscreen().catch((e) => {
console.error('Fullscreen request failed:', e); console.error('Fullscreen request failed:', e);
}); });
} else if ((videoEl as HTMLVideoElement & { webkitEnterFullscreen?: () => void }).webkitEnterFullscreen) { } else if ((el as HTMLElement & { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen) {
(videoEl as HTMLVideoElement & { webkitEnterFullscreen: () => void }).webkitEnterFullscreen(); (el as HTMLElement & { webkitRequestFullscreen: () => void }).webkitRequestFullscreen();
} }
} }
} }

View File

@@ -19,6 +19,7 @@ import {
isFullscreenActive, isFullscreenActive,
setPlaybackRate, setPlaybackRate,
getPlaybackRate, getPlaybackRate,
adjustShuttle,
} from '$lib/transport/playback'; } from '$lib/transport/playback';
function createMockVideoElement(): HTMLVideoElement { function createMockVideoElement(): HTMLVideoElement {
@@ -85,9 +86,11 @@ describe('playback — fullscreen helpers', () => {
expect(isFullscreenActive()).toBe(false); expect(isFullscreenActive()).toBe(false);
}); });
it('toggleFullscreen calls requestFullscreen when not fullscreen', () => { it('toggleFullscreen calls requestFullscreen on document.documentElement', () => {
const spy = vi.fn().mockResolvedValue(undefined);
document.documentElement.requestFullscreen = spy;
toggleFullscreen(); toggleFullscreen();
expect(mockVideo.requestFullscreen).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it('isFullscreenActive returns true when fullscreen element exists', () => { it('isFullscreenActive returns true when fullscreen element exists', () => {
@@ -127,3 +130,59 @@ describe('playback — playback rate helpers', () => {
expect(getPlaybackRate()).toBe(1); expect(getPlaybackRate()).toBe(1);
}); });
}); });
describe('playback — adjustShuttle', () => {
let mockVideo: HTMLVideoElement;
beforeEach(() => {
mockVideo = createMockVideoElement();
setVideoElement(mockVideo);
mockVideo.playbackRate = 1;
Object.defineProperty(mockVideo, 'paused', {
value: true,
writable: true,
configurable: true,
});
mockVideo.play = vi.fn().mockResolvedValue(undefined);
});
it('increases rate by 0.25 when dir is 1', () => {
const result = adjustShuttle(1);
expect(result).toBe(1.25);
expect(mockVideo.playbackRate).toBe(1.25);
});
it('decreases rate by 0.25 when dir is -1', () => {
const result = adjustShuttle(-1);
expect(result).toBe(0.75);
expect(mockVideo.playbackRate).toBe(0.75);
});
it('reads current rate from video element, not external state', () => {
mockVideo.playbackRate = 0.75;
const result = adjustShuttle(1);
expect(result).toBe(1);
});
it('clamps to minimum 0.25', () => {
mockVideo.playbackRate = 0.25;
const result = adjustShuttle(-1);
expect(result).toBe(0.25);
});
it('clamps to maximum 4', () => {
mockVideo.playbackRate = 4;
const result = adjustShuttle(1);
expect(result).toBe(4);
});
it('starts playback if paused', () => {
adjustShuttle(1);
expect(mockVideo.play).toHaveBeenCalled();
});
it('returns 1 when no video element', () => {
setVideoElement(null);
expect(adjustShuttle(1)).toBe(1);
});
});