fix: adjustShuttle reads live rate with 0.25 step, fullscreen targets documentElement
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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': {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user