feat: add PiP, fullscreen, and playbackRate helpers to playback module
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,6 +96,58 @@ export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number {
|
|||||||
return nextRate;
|
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 =
|
export type TransportKeyAction =
|
||||||
| 'play-pause'
|
| 'play-pause'
|
||||||
| 'frame-back'
|
| 'frame-back'
|
||||||
|
|||||||
129
tests/lib/transport/playback.test.ts
Normal file
129
tests/lib/transport/playback.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user