mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: small state and naming fixes (#719)
This commit is contained in:
@@ -62,13 +62,13 @@ export interface MediaVolumeState {
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
|
||||
*/
|
||||
changeVolume(volume: number): number;
|
||||
setVolume(volume: number): number;
|
||||
/**
|
||||
* Toggle mute state. Returns the new muted value.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted
|
||||
*/
|
||||
toggleMute(): boolean;
|
||||
toggleMuted(): boolean;
|
||||
}
|
||||
|
||||
export interface MediaTimeState {
|
||||
@@ -224,11 +224,11 @@ export interface MediaPictureInPictureState {
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture
|
||||
*/
|
||||
requestPiP(): Promise<void>;
|
||||
requestPictureInPicture(): Promise<void>;
|
||||
/**
|
||||
* Exit picture-in-picture mode.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/exitPictureInPicture
|
||||
*/
|
||||
exitPiP(): Promise<void>;
|
||||
exitPictureInPicture(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class MuteButtonCore {
|
||||
|
||||
toggle(media: MediaVolumeState): void {
|
||||
if (this.#props.disabled) return;
|
||||
media.toggleMute();
|
||||
media.toggleMuted();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ function createMediaState(overrides: Partial<MediaVolumeState> = {}): MediaVolum
|
||||
volume: 1,
|
||||
muted: false,
|
||||
volumeAvailability: 'available',
|
||||
changeVolume: vi.fn((v: number) => v),
|
||||
toggleMute: vi.fn(() => false),
|
||||
setVolume: vi.fn((v: number) => v),
|
||||
toggleMuted: vi.fn(() => false),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -108,18 +108,18 @@ describe('MuteButtonCore', () => {
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
it('calls toggleMute', () => {
|
||||
it('calls toggleMuted', () => {
|
||||
const core = new MuteButtonCore();
|
||||
const media = createMediaState();
|
||||
core.toggle(media);
|
||||
expect(media.toggleMute).toHaveBeenCalled();
|
||||
expect(media.toggleMuted).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when disabled', () => {
|
||||
const core = new MuteButtonCore({ disabled: true });
|
||||
const media = createMediaState();
|
||||
core.toggle(media);
|
||||
expect(media.toggleMute).not.toHaveBeenCalled();
|
||||
expect(media.toggleMuted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,9 +71,9 @@ export class PiPButtonCore {
|
||||
|
||||
try {
|
||||
if (media.pip) {
|
||||
await media.exitPiP();
|
||||
await media.exitPictureInPicture();
|
||||
} else {
|
||||
await media.requestPiP();
|
||||
await media.requestPictureInPicture();
|
||||
}
|
||||
} catch {
|
||||
// PiP requests can fail (user gesture required, permissions, etc.)
|
||||
|
||||
@@ -8,8 +8,8 @@ function createMediaState(overrides: Partial<MediaPictureInPictureState> = {}):
|
||||
return {
|
||||
pip: false,
|
||||
pipAvailability: 'available',
|
||||
requestPiP: vi.fn(async () => {}),
|
||||
exitPiP: vi.fn(async () => {}),
|
||||
requestPictureInPicture: vi.fn(async () => {}),
|
||||
exitPictureInPicture: vi.fn(async () => {}),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -82,38 +82,38 @@ describe('PiPButtonCore', () => {
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
it('calls requestPiP when not in PiP', async () => {
|
||||
it('calls requestPictureInPicture when not in PiP', async () => {
|
||||
const core = new PiPButtonCore();
|
||||
const media = createMediaState({ pip: false });
|
||||
await core.toggle(media);
|
||||
expect(media.requestPiP).toHaveBeenCalled();
|
||||
expect(media.requestPictureInPicture).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls exitPiP when in PiP', async () => {
|
||||
it('calls exitPictureInPicture when in PiP', async () => {
|
||||
const core = new PiPButtonCore();
|
||||
const media = createMediaState({ pip: true });
|
||||
await core.toggle(media);
|
||||
expect(media.exitPiP).toHaveBeenCalled();
|
||||
expect(media.exitPictureInPicture).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when disabled', async () => {
|
||||
const core = new PiPButtonCore({ disabled: true });
|
||||
const media = createMediaState();
|
||||
await core.toggle(media);
|
||||
expect(media.requestPiP).not.toHaveBeenCalled();
|
||||
expect(media.requestPictureInPicture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when unsupported', async () => {
|
||||
const core = new PiPButtonCore();
|
||||
const media = createMediaState({ pipAvailability: 'unsupported' });
|
||||
await core.toggle(media);
|
||||
expect(media.requestPiP).not.toHaveBeenCalled();
|
||||
expect(media.requestPictureInPicture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('catches PiP errors silently', async () => {
|
||||
const core = new PiPButtonCore();
|
||||
const media = createMediaState({
|
||||
requestPiP: vi.fn(async () => {
|
||||
requestPictureInPicture: vi.fn(async () => {
|
||||
throw new Error('permission denied');
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@ function createMediaState(overrides: Partial<MediaVolumeState> = {}): MediaVolum
|
||||
volume: 1,
|
||||
muted: false,
|
||||
volumeAvailability: 'available',
|
||||
changeVolume: vi.fn((v: number) => v),
|
||||
toggleMute: vi.fn(() => false),
|
||||
setVolume: vi.fn((v: number) => v),
|
||||
toggleMuted: vi.fn(() => false),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isFunction } from '@videojs/utils/predicate';
|
||||
import type { WebKitDocument, WebKitFullscreenElement, WebKitVideoElement } from './types';
|
||||
|
||||
/** Check if the Fullscreen API is supported on this platform. */
|
||||
export function isFullscreenSupported(): boolean {
|
||||
export function isFullscreenEnabled(): boolean {
|
||||
const doc = document as WebKitDocument;
|
||||
|
||||
// Standard API or WebKit prefix
|
||||
@@ -27,7 +27,7 @@ export function getFullscreenElement(): Element | null {
|
||||
*
|
||||
* Uses `:fullscreen` pseudo-class which works across Shadow DOM boundaries.
|
||||
*/
|
||||
export function isElementFullscreen(container: HTMLElement | null, media: HTMLMediaElement): boolean {
|
||||
export function isFullscreenElement(container: HTMLElement | null, media: HTMLMediaElement): boolean {
|
||||
const video = media as WebKitVideoElement;
|
||||
|
||||
// iOS Safari video-only fullscreen
|
||||
@@ -49,12 +49,12 @@ export function isElementFullscreen(container: HTMLElement | null, media: HTMLMe
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter fullscreen mode.
|
||||
* Request fullscreen mode.
|
||||
*
|
||||
* Tries container first (to show custom UI), falls back to media element
|
||||
* for platforms that only support video fullscreen (iOS Safari).
|
||||
*/
|
||||
export async function enterFullscreen(container: HTMLElement | null, media: HTMLMediaElement): Promise<void> {
|
||||
export async function requestFullscreen(container: HTMLElement | null, media: HTMLMediaElement): Promise<void> {
|
||||
const video = media as WebKitVideoElement;
|
||||
|
||||
// Try container first (standard and WebKit APIs)
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { WebKitVideoElement } from './types';
|
||||
*
|
||||
* Note: Safari PWAs don't support PiP even though the API exists.
|
||||
*/
|
||||
export function isPiPSupported(): boolean {
|
||||
export function isPictureInPictureEnabled(): boolean {
|
||||
// Check standard PiP API
|
||||
if (document.pictureInPictureEnabled) {
|
||||
// Safari PWAs have the API but it doesn't work
|
||||
@@ -24,7 +24,7 @@ export function isPiPSupported(): boolean {
|
||||
/**
|
||||
* Check if Picture-in-Picture is currently active for a media element.
|
||||
*/
|
||||
export function isPiPActive(media: HTMLMediaElement): boolean {
|
||||
export function isPictureInPictureElement(media: HTMLMediaElement): boolean {
|
||||
// Standard PiP API
|
||||
if (document.pictureInPictureElement === media) {
|
||||
return true;
|
||||
@@ -36,12 +36,12 @@ export function isPiPActive(media: HTMLMediaElement): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter Picture-in-Picture mode.
|
||||
* Request Picture-in-Picture mode.
|
||||
*
|
||||
* Uses standard API where available, falls back to iOS Safari's
|
||||
* WebKit presentation mode.
|
||||
*/
|
||||
export async function enterPiP(media: HTMLMediaElement): Promise<void> {
|
||||
export async function requestPictureInPicture(media: HTMLMediaElement): Promise<void> {
|
||||
const video = media as HTMLVideoElement & WebKitVideoElement;
|
||||
|
||||
// Standard PiP API (only available on HTMLVideoElement)
|
||||
@@ -65,7 +65,7 @@ export async function enterPiP(media: HTMLMediaElement): Promise<void> {
|
||||
* Uses standard API where available, falls back to iOS Safari's
|
||||
* WebKit presentation mode.
|
||||
*/
|
||||
export async function exitPiP(media?: HTMLMediaElement): Promise<void> {
|
||||
export async function exitPictureInPicture(media?: HTMLMediaElement): Promise<void> {
|
||||
// Standard PiP API
|
||||
if (document.pictureInPictureElement && isFunction(document.exitPictureInPicture)) {
|
||||
await document.exitPictureInPicture();
|
||||
|
||||
@@ -3,12 +3,12 @@ import { listen } from '@videojs/utils/dom';
|
||||
import type { MediaFullscreenState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
import {
|
||||
enterFullscreen,
|
||||
exitFullscreen,
|
||||
isElementFullscreen,
|
||||
isFullscreenSupported,
|
||||
isFullscreenElement,
|
||||
isFullscreenEnabled,
|
||||
requestFullscreen,
|
||||
} from '../../presentation/fullscreen';
|
||||
import { exitPiP, isPiPActive } from '../../presentation/pip';
|
||||
import { exitPictureInPicture, isPictureInPictureElement } from '../../presentation/pip';
|
||||
import type { WebKitVideoElement } from '../../presentation/types';
|
||||
|
||||
export const fullscreenFeature = definePlayerFeature({
|
||||
@@ -21,11 +21,11 @@ export const fullscreenFeature = definePlayerFeature({
|
||||
const { media, container } = target();
|
||||
|
||||
// Exit PiP first if active (browser behavior is inconsistent)
|
||||
if (isPiPActive(media)) {
|
||||
await exitPiP(media);
|
||||
if (isPictureInPictureElement(media)) {
|
||||
await exitPictureInPicture(media);
|
||||
}
|
||||
|
||||
return enterFullscreen(container, media);
|
||||
return requestFullscreen(container, media);
|
||||
},
|
||||
|
||||
async exitFullscreen() {
|
||||
@@ -37,12 +37,12 @@ export const fullscreenFeature = definePlayerFeature({
|
||||
const { media, container } = target;
|
||||
|
||||
set({
|
||||
fullscreenAvailability: isFullscreenSupported() ? 'available' : 'unsupported',
|
||||
fullscreenAvailability: isFullscreenEnabled() ? 'available' : 'unsupported',
|
||||
});
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
fullscreen: isElementFullscreen(container, media),
|
||||
fullscreen: isFullscreenElement(container, media),
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
@@ -2,8 +2,13 @@ import { listen } from '@videojs/utils/dom';
|
||||
|
||||
import type { MediaPictureInPictureState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
import { exitFullscreen, isElementFullscreen } from '../../presentation/fullscreen';
|
||||
import { enterPiP, exitPiP, isPiPActive, isPiPSupported } from '../../presentation/pip';
|
||||
import { exitFullscreen, isFullscreenElement } from '../../presentation/fullscreen';
|
||||
import {
|
||||
exitPictureInPicture,
|
||||
isPictureInPictureElement,
|
||||
isPictureInPictureEnabled,
|
||||
requestPictureInPicture,
|
||||
} from '../../presentation/pip';
|
||||
import type { WebKitVideoElement } from '../../presentation/types';
|
||||
|
||||
export const pipFeature = definePlayerFeature({
|
||||
@@ -12,20 +17,20 @@ export const pipFeature = definePlayerFeature({
|
||||
pip: false,
|
||||
pipAvailability: 'unavailable',
|
||||
|
||||
async requestPiP() {
|
||||
async requestPictureInPicture() {
|
||||
const { media, container } = target();
|
||||
|
||||
// Exit fullscreen first if active
|
||||
if (isElementFullscreen(container, media)) {
|
||||
if (isFullscreenElement(container, media)) {
|
||||
await exitFullscreen();
|
||||
}
|
||||
|
||||
return enterPiP(media);
|
||||
return requestPictureInPicture(media);
|
||||
},
|
||||
|
||||
async exitPiP() {
|
||||
async exitPictureInPicture() {
|
||||
const { media } = target();
|
||||
return exitPiP(media);
|
||||
return exitPictureInPicture(media);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -33,12 +38,12 @@ export const pipFeature = definePlayerFeature({
|
||||
const { media } = target;
|
||||
|
||||
set({
|
||||
pipAvailability: isPiPSupported() ? 'available' : 'unsupported',
|
||||
pipAvailability: isPictureInPictureEnabled() ? 'available' : 'unsupported',
|
||||
});
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
pip: isPiPActive(media),
|
||||
pip: isPictureInPictureElement(media),
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
@@ -31,6 +31,7 @@ export const playbackFeature = definePlayerFeature({
|
||||
|
||||
sync();
|
||||
|
||||
listen(media, 'emptied', sync, { signal });
|
||||
listen(media, 'play', sync, { signal });
|
||||
listen(media, 'pause', sync, { signal });
|
||||
listen(media, 'ended', sync, { signal });
|
||||
|
||||
@@ -85,19 +85,19 @@ describe('pipFeature', () => {
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('requestPiP() calls requestPictureInPicture on video', async () => {
|
||||
it('requestPictureInPicture() calls requestPictureInPicture on video', async () => {
|
||||
const video = createMockVideo();
|
||||
video.requestPictureInPicture = vi.fn().mockResolvedValue({});
|
||||
|
||||
const store = createStore<PlayerTarget>()(pipFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.requestPiP();
|
||||
await store.requestPictureInPicture();
|
||||
|
||||
expect(video.requestPictureInPicture).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exitPiP() calls document.exitPictureInPicture', async () => {
|
||||
it('exitPictureInPicture() calls document.exitPictureInPicture', async () => {
|
||||
const originalExit = document.exitPictureInPicture;
|
||||
document.exitPictureInPicture = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('pipFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(pipFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.exitPiP();
|
||||
await store.exitPictureInPicture();
|
||||
|
||||
expect(document.exitPictureInPicture).toHaveBeenCalled();
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('pipFeature', () => {
|
||||
});
|
||||
|
||||
describe('transitions', () => {
|
||||
it('requestPiP() exits fullscreen first if active', async () => {
|
||||
it('requestPictureInPicture() exits fullscreen first if active', async () => {
|
||||
const originalExit = document.exitFullscreen;
|
||||
document.exitFullscreen = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('pipFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(pipFeature);
|
||||
store.attach({ media: video, container });
|
||||
|
||||
await store.requestPiP();
|
||||
await store.requestPictureInPicture();
|
||||
|
||||
expect(document.exitFullscreen).toHaveBeenCalled();
|
||||
expect(video.requestPictureInPicture).toHaveBeenCalled();
|
||||
@@ -148,7 +148,7 @@ describe('pipFeature', () => {
|
||||
document.exitFullscreen = originalExit;
|
||||
});
|
||||
|
||||
it('requestPiP() does not exit fullscreen if not active', async () => {
|
||||
it('requestPictureInPicture() does not exit fullscreen if not active', async () => {
|
||||
const originalExit = document.exitFullscreen;
|
||||
document.exitFullscreen = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('pipFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(pipFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.requestPiP();
|
||||
await store.requestPictureInPicture();
|
||||
|
||||
expect(document.exitFullscreen).not.toHaveBeenCalled();
|
||||
expect(video.requestPictureInPicture).toHaveBeenCalled();
|
||||
|
||||
@@ -47,13 +47,13 @@ describe('volumeFeature', () => {
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('changeVolume', () => {
|
||||
describe('setVolume', () => {
|
||||
it('sets volume on target', async () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore<PlayerTarget>()(volumeFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.changeVolume(0.7);
|
||||
const result = await store.setVolume(0.7);
|
||||
|
||||
expect(video.volume).toBe(0.7);
|
||||
expect(result).toBe(0.7);
|
||||
@@ -64,7 +64,7 @@ describe('volumeFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(volumeFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.changeVolume(-0.5);
|
||||
await store.setVolume(-0.5);
|
||||
|
||||
expect(video.volume).toBe(0);
|
||||
});
|
||||
@@ -74,19 +74,19 @@ describe('volumeFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(volumeFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.changeVolume(1.5);
|
||||
await store.setVolume(1.5);
|
||||
|
||||
expect(video.volume).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleMute', () => {
|
||||
describe('toggleMuted', () => {
|
||||
it('toggles mute from false to true', async () => {
|
||||
const video = createMockVideo({ muted: false });
|
||||
const store = createStore<PlayerTarget>()(volumeFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.toggleMute();
|
||||
const result = await store.toggleMuted();
|
||||
|
||||
expect(video.muted).toBe(true);
|
||||
expect(result).toBe(true);
|
||||
@@ -97,7 +97,7 @@ describe('volumeFeature', () => {
|
||||
const store = createStore<PlayerTarget>()(volumeFeature);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.toggleMute();
|
||||
const result = await store.toggleMuted();
|
||||
|
||||
expect(video.muted).toBe(false);
|
||||
expect(result).toBe(false);
|
||||
|
||||
@@ -10,13 +10,13 @@ export const volumeFeature = definePlayerFeature({
|
||||
muted: false,
|
||||
volumeAvailability: 'unavailable',
|
||||
|
||||
changeVolume(volume: number) {
|
||||
setVolume(volume: number) {
|
||||
const { media } = target();
|
||||
media.volume = Math.max(0, Math.min(1, volume));
|
||||
return media.volume;
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
toggleMuted() {
|
||||
const { media } = target();
|
||||
media.muted = !media.muted;
|
||||
return media.muted;
|
||||
|
||||
Reference in New Issue
Block a user