feat: add PiP, fullscreen, and playbackRate helpers to playback module

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 19:02:03 -04:00
parent bd96da4dc9
commit d9f58747a4
2 changed files with 181 additions and 0 deletions

View File

@@ -96,6 +96,58 @@ export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number {
return nextRate;
}
export function togglePiP(): void {
if (document.pictureInPictureElement) {
document.exitPictureInPicture().catch((e) => {
console.error('Exit PiP failed:', e);
});
} else {
const videoEl = getVideo();
if (videoEl && document.pictureInPictureEnabled) {
videoEl.requestPictureInPicture().catch((e) => {
console.error('PiP request failed:', e);
});
}
}
}
export function isPiPActive(): boolean {
return !!document.pictureInPictureElement;
}
export function toggleFullscreen(): void {
if (document.fullscreenElement) {
document.exitFullscreen().catch((e) => {
console.error('Exit fullscreen failed:', e);
});
} else {
const videoEl = getVideo();
if (!videoEl) return;
if (videoEl.requestFullscreen) {
videoEl.requestFullscreen().catch((e) => {
console.error('Fullscreen request failed:', e);
});
} else if ((videoEl as HTMLVideoElement & { webkitEnterFullscreen?: () => void }).webkitEnterFullscreen) {
(videoEl as HTMLVideoElement & { webkitEnterFullscreen: () => void }).webkitEnterFullscreen();
}
}
}
export function isFullscreenActive(): boolean {
return !!document.fullscreenElement;
}
export function setPlaybackRate(rate: number): void {
const videoEl = getVideo();
if (videoEl) {
videoEl.playbackRate = rate;
}
}
export function getPlaybackRate(): number {
return getVideo()?.playbackRate ?? 1;
}
export type TransportKeyAction =
| 'play-pause'
| 'frame-back'

View File

@@ -0,0 +1,129 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock the videoSession store (playback.ts imports it)
vi.mock('$lib/stores/videoSession.svelte', () => ({
session: {
duration: 120,
currentTime: 0,
fps: 30,
isPlaying: false,
keyframePositions: [],
},
}));
import {
setVideoElement,
togglePiP,
isPiPActive,
toggleFullscreen,
isFullscreenActive,
setPlaybackRate,
getPlaybackRate,
} from '$lib/transport/playback';
function createMockVideoElement(): HTMLVideoElement {
const el = document.createElement('video');
// PiP
(el as any).requestPictureInPicture = vi.fn().mockResolvedValue({});
// Fullscreen
el.requestFullscreen = vi.fn().mockResolvedValue(undefined);
return el;
}
describe('playback — PiP helpers', () => {
let mockVideo: HTMLVideoElement;
beforeEach(() => {
mockVideo = createMockVideoElement();
setVideoElement(mockVideo);
// Reset PiP state
Object.defineProperty(document, 'pictureInPictureElement', {
value: null,
writable: true,
configurable: true,
});
Object.defineProperty(document, 'pictureInPictureEnabled', {
value: true,
writable: true,
configurable: true,
});
});
it('isPiPActive returns false when no PiP element', () => {
expect(isPiPActive()).toBe(false);
});
it('togglePiP calls requestPictureInPicture', () => {
togglePiP();
expect((mockVideo as any).requestPictureInPicture).toHaveBeenCalled();
});
it('isPiPActive returns true when PiP element exists', () => {
Object.defineProperty(document, 'pictureInPictureElement', {
value: mockVideo,
writable: true,
configurable: true,
});
expect(isPiPActive()).toBe(true);
});
});
describe('playback — fullscreen helpers', () => {
let mockVideo: HTMLVideoElement;
beforeEach(() => {
mockVideo = createMockVideoElement();
setVideoElement(mockVideo);
Object.defineProperty(document, 'fullscreenElement', {
value: null,
writable: true,
configurable: true,
});
});
it('isFullscreenActive returns false when not fullscreen', () => {
expect(isFullscreenActive()).toBe(false);
});
it('toggleFullscreen calls requestFullscreen when not fullscreen', () => {
toggleFullscreen();
expect(mockVideo.requestFullscreen).toHaveBeenCalled();
});
it('isFullscreenActive returns true when fullscreen element exists', () => {
Object.defineProperty(document, 'fullscreenElement', {
value: mockVideo,
writable: true,
configurable: true,
});
expect(isFullscreenActive()).toBe(true);
});
});
describe('playback — playback rate helpers', () => {
let mockVideo: HTMLVideoElement;
beforeEach(() => {
mockVideo = createMockVideoElement();
setVideoElement(mockVideo);
});
it('getPlaybackRate returns 1 by default', () => {
expect(getPlaybackRate()).toBe(1);
});
it('setPlaybackRate changes the video playback rate', () => {
setPlaybackRate(1.5);
expect(mockVideo.playbackRate).toBe(1.5);
});
it('getPlaybackRate returns the current rate', () => {
setPlaybackRate(2);
expect(getPlaybackRate()).toBe(2);
});
it('getPlaybackRate returns 1 when no video element', () => {
setVideoElement(null);
expect(getPlaybackRate()).toBe(1);
});
});