mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(core): add player target and feature selectors (#371)
This commit is contained in:
@@ -464,6 +464,17 @@ export function onEvent<K extends keyof HTMLElementEventMap>(...): Promise<...>;
|
||||
export function supportsIdleCallback(): boolean { ... }
|
||||
get size(): number { ... }
|
||||
add(cleanup: CleanupFn): void { ... }
|
||||
|
||||
// Bad - comment restates the obvious
|
||||
/** Media element contract. */
|
||||
export interface Media extends HTMLMediaElement {}
|
||||
|
||||
/** Feature capability availability. */
|
||||
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
|
||||
|
||||
// Good - no comment needed
|
||||
export interface Media extends HTMLMediaElement {}
|
||||
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
|
||||
```
|
||||
|
||||
## Design Documents
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './store/features';
|
||||
export * from './store/selectors';
|
||||
export * from './types';
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { InferFeatureState } from '@videojs/store';
|
||||
import { defineFeature } from '@videojs/store';
|
||||
import { listen, serializeTimeRanges } from '@videojs/utils/dom';
|
||||
|
||||
export const bufferFeature = defineFeature<HTMLMediaElement>()({
|
||||
import type { PlayerTarget } from '../../types';
|
||||
|
||||
export const bufferFeature = defineFeature<PlayerTarget>()({
|
||||
state: () => ({
|
||||
/** Buffered time ranges as [start, end] tuples. */
|
||||
buffered: [] as [number, number][],
|
||||
@@ -12,16 +14,18 @@ export const bufferFeature = defineFeature<HTMLMediaElement>()({
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const { media } = target;
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
buffered: serializeTimeRanges(target.buffered),
|
||||
seekable: serializeTimeRanges(target.seekable),
|
||||
buffered: serializeTimeRanges(media.buffered),
|
||||
seekable: serializeTimeRanges(media.seekable),
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
listen(target, 'progress', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
listen(media, 'progress', sync, { signal });
|
||||
listen(media, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import { sourceFeature } from './source';
|
||||
import { timeFeature } from './time';
|
||||
import { volumeFeature } from './volume';
|
||||
|
||||
// Short aliases
|
||||
export {
|
||||
bufferFeature as buffer,
|
||||
playbackFeature as playback,
|
||||
@@ -12,4 +13,6 @@ export {
|
||||
volumeFeature as volume,
|
||||
};
|
||||
|
||||
export const all = [bufferFeature, playbackFeature, sourceFeature, timeFeature, volumeFeature] as const;
|
||||
export const video = [playbackFeature, volumeFeature, timeFeature, sourceFeature, bufferFeature] as const;
|
||||
|
||||
export const audio = [playbackFeature, volumeFeature, timeFeature, sourceFeature, bufferFeature] as const;
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './buffer';
|
||||
export * as media from './media.parts';
|
||||
export * as features from './feature.parts';
|
||||
export * from './playback';
|
||||
export * from './source';
|
||||
export * from './time';
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { InferFeatureState } from '@videojs/store';
|
||||
import { defineFeature } from '@videojs/store';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
|
||||
export const playbackFeature = defineFeature<HTMLMediaElement>()({
|
||||
import type { PlayerTarget } from '../../types';
|
||||
|
||||
export const playbackFeature = defineFeature<PlayerTarget>()({
|
||||
state: ({ task }) => ({
|
||||
/** Whether playback is paused. */
|
||||
paused: true,
|
||||
@@ -20,7 +22,7 @@ export const playbackFeature = defineFeature<HTMLMediaElement>()({
|
||||
key: 'playback',
|
||||
mode: 'shared',
|
||||
async handler({ target }) {
|
||||
await target.play();
|
||||
await target.media.play();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -30,28 +32,30 @@ export const playbackFeature = defineFeature<HTMLMediaElement>()({
|
||||
return task({
|
||||
key: 'playback',
|
||||
handler({ target }) {
|
||||
target.pause();
|
||||
target.media.pause();
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const { media } = target;
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
paused: target.paused,
|
||||
ended: target.ended,
|
||||
started: !target.paused || target.currentTime > 0,
|
||||
waiting: target.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !target.paused,
|
||||
paused: media.paused,
|
||||
ended: media.ended,
|
||||
started: !media.paused || media.currentTime > 0,
|
||||
waiting: media.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !media.paused,
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
listen(target, 'play', sync, { signal });
|
||||
listen(target, 'pause', sync, { signal });
|
||||
listen(target, 'ended', sync, { signal });
|
||||
listen(target, 'playing', sync, { signal });
|
||||
listen(target, 'waiting', sync, { signal });
|
||||
listen(media, 'play', sync, { signal });
|
||||
listen(media, 'pause', sync, { signal });
|
||||
listen(media, 'ended', sync, { signal });
|
||||
listen(media, 'playing', sync, { signal });
|
||||
listen(media, 'waiting', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { InferFeatureState } from '@videojs/store';
|
||||
import { CANCEL_ALL, defineFeature } from '@videojs/store';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
|
||||
export const sourceFeature = defineFeature<HTMLMediaElement>()({
|
||||
import type { PlayerTarget } from '../../types';
|
||||
|
||||
export const sourceFeature = defineFeature<PlayerTarget>()({
|
||||
state: ({ task }) => ({
|
||||
/** Current media source URL (null if none). */
|
||||
source: null as string | null,
|
||||
@@ -16,8 +18,8 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
|
||||
key: 'source',
|
||||
cancels: [CANCEL_ALL],
|
||||
handler({ target }) {
|
||||
target.src = src;
|
||||
target.load();
|
||||
target.media.src = src;
|
||||
target.media.load();
|
||||
return src;
|
||||
},
|
||||
});
|
||||
@@ -25,18 +27,20 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const { media } = target;
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
source: target.currentSrc || target.src || null,
|
||||
canPlay: target.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
source: media.currentSrc || media.src || null,
|
||||
canPlay: media.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
listen(target, 'canplay', sync, { signal });
|
||||
listen(target, 'canplaythrough', sync, { signal });
|
||||
listen(target, 'loadstart', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
listen(media, 'canplay', sync, { signal });
|
||||
listen(media, 'canplaythrough', sync, { signal });
|
||||
listen(media, 'loadstart', sync, { signal });
|
||||
listen(media, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('bufferFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.buffered).toEqual([[0, 60]]);
|
||||
expect(store.state.seekable).toEqual([[0, 120]]);
|
||||
@@ -28,7 +28,7 @@ describe('bufferFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.buffered).toEqual([
|
||||
[0, 30],
|
||||
@@ -43,7 +43,7 @@ describe('bufferFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
// Update the mock video's buffered range
|
||||
Object.defineProperty(video, 'buffered', {
|
||||
@@ -64,7 +64,7 @@ describe('bufferFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [bufferFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
// Update the mock video to have no buffered content
|
||||
Object.defineProperty(video, 'buffered', {
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('playbackFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.paused).toBe(false);
|
||||
expect(store.state.ended).toBe(false);
|
||||
@@ -29,7 +29,7 @@ describe('playbackFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.waiting).toBe(true);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe('playbackFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.started).toBe(true);
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe('playbackFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.started).toBe(true);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ describe('playbackFeature', () => {
|
||||
const video = createMockVideo({ paused: true });
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.paused).toBe(true);
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('playbackFeature', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.paused).toBe(false);
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('playbackFeature', () => {
|
||||
const video = createMockVideo({ ended: false });
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.ended).toBe(false);
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('playbackFeature', () => {
|
||||
const video = createMockVideo({});
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
store.destroy();
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('playbackFeature', () => {
|
||||
video.play = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.play();
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('playbackFeature', () => {
|
||||
video.pause = vi.fn();
|
||||
|
||||
const store = createStore({ features: [playbackFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
store.pause();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('sourceFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.source).toBe('https://example.com/video.mp4');
|
||||
expect(store.state.canPlay).toBe(true);
|
||||
@@ -26,7 +26,7 @@ describe('sourceFeature', () => {
|
||||
Object.defineProperty(video, 'readyState', { value: HTMLMediaElement.HAVE_NOTHING, writable: false });
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.source).toBe(null);
|
||||
expect(store.state.canPlay).toBe(false);
|
||||
@@ -39,7 +39,7 @@ describe('sourceFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.canPlay).toBe(false);
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('sourceFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.source).toBe('https://example.com/video.mp4');
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('sourceFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.canPlay).toBe(true);
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('sourceFeature', () => {
|
||||
video.load = vi.fn();
|
||||
|
||||
const store = createStore({ features: [sourceFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.loadSource('https://example.com/new.mp4');
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('timeFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.currentTime).toBe(30);
|
||||
expect(store.state.duration).toBe(120);
|
||||
@@ -25,7 +25,7 @@ describe('timeFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.duration).toBe(0);
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe('timeFeature', () => {
|
||||
const video = createMockVideo({ currentTime: 0 });
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.currentTime).toBe(0);
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('timeFeature', () => {
|
||||
const video = createMockVideo({ duration: 0 });
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.duration).toBe(0);
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('timeFeature', () => {
|
||||
const video = createMockVideo({ currentTime: 0 });
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
// Update mock currentTime
|
||||
video.currentTime = 50;
|
||||
@@ -80,7 +80,7 @@ describe('timeFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
// Update mock to empty state
|
||||
video.currentTime = 0;
|
||||
@@ -97,7 +97,7 @@ describe('timeFeature', () => {
|
||||
it('sets currentTime on target and waits for seeked event', async () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore({ features: [timeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const resultPromise = store.seek(45);
|
||||
|
||||
|
||||
@@ -12,17 +12,26 @@ describe('volumeFeature', () => {
|
||||
});
|
||||
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.volume).toBe(0.8);
|
||||
expect(store.state.muted).toBe(false);
|
||||
});
|
||||
|
||||
it('sets volumeAvailability on attach', () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
// Should be 'available' or 'unsupported' based on browser capability
|
||||
expect(['available', 'unsupported']).toContain(store.state.volumeAvailability);
|
||||
});
|
||||
|
||||
it('updates on volumechange event', () => {
|
||||
const video = createMockVideo({ volume: 1, muted: false });
|
||||
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
expect(store.state.volume).toBe(1);
|
||||
|
||||
@@ -41,7 +50,7 @@ describe('volumeFeature', () => {
|
||||
it('sets volume on target', async () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.changeVolume(0.7);
|
||||
|
||||
@@ -52,7 +61,7 @@ describe('volumeFeature', () => {
|
||||
it('clamps volume to min 0', async () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.changeVolume(-0.5);
|
||||
|
||||
@@ -62,7 +71,7 @@ describe('volumeFeature', () => {
|
||||
it('clamps volume to max 1', async () => {
|
||||
const video = createMockVideo({});
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
await store.changeVolume(1.5);
|
||||
|
||||
@@ -74,7 +83,7 @@ describe('volumeFeature', () => {
|
||||
it('toggles mute from false to true', async () => {
|
||||
const video = createMockVideo({ muted: false });
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.toggleMute();
|
||||
|
||||
@@ -85,7 +94,7 @@ describe('volumeFeature', () => {
|
||||
it('toggles mute from true to false', async () => {
|
||||
const video = createMockVideo({ muted: true });
|
||||
const store = createStore({ features: [volumeFeature] });
|
||||
store.attach(video);
|
||||
store.attach({ media: video, container: null });
|
||||
|
||||
const result = await store.toggleMute();
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { InferFeatureState } from '@videojs/store';
|
||||
import { defineFeature } from '@videojs/store';
|
||||
import { listen, onEvent } from '@videojs/utils/dom';
|
||||
|
||||
export const timeFeature = defineFeature<HTMLMediaElement>()({
|
||||
import type { PlayerTarget } from '../../types';
|
||||
|
||||
export const timeFeature = defineFeature<PlayerTarget>()({
|
||||
state: ({ task }) => ({
|
||||
/** Current playback position in seconds. */
|
||||
currentTime: 0,
|
||||
@@ -15,28 +17,30 @@ export const timeFeature = defineFeature<HTMLMediaElement>()({
|
||||
return task({
|
||||
key: 'seek',
|
||||
async handler({ target, signal }) {
|
||||
target.currentTime = time;
|
||||
await onEvent(target, 'seeked', { signal });
|
||||
return target.currentTime; // actual position after seek
|
||||
target.media.currentTime = time;
|
||||
await onEvent(target.media, 'seeked', { signal });
|
||||
return target.media.currentTime; // actual position after seek
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const { media } = target;
|
||||
|
||||
const sync = () =>
|
||||
set({
|
||||
currentTime: target.currentTime,
|
||||
duration: target.duration || 0,
|
||||
currentTime: media.currentTime,
|
||||
duration: media.duration || 0,
|
||||
});
|
||||
|
||||
sync();
|
||||
|
||||
listen(target, 'timeupdate', sync, { signal });
|
||||
listen(target, 'durationchange', sync, { signal });
|
||||
listen(target, 'seeked', sync, { signal });
|
||||
listen(target, 'loadedmetadata', sync, { signal });
|
||||
listen(target, 'emptied', sync, { signal });
|
||||
listen(media, 'timeupdate', sync, { signal });
|
||||
listen(media, 'durationchange', sync, { signal });
|
||||
listen(media, 'seeked', sync, { signal });
|
||||
listen(media, 'loadedmetadata', sync, { signal });
|
||||
listen(media, 'emptied', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,20 +3,24 @@ import type { InferFeatureState } from '@videojs/store';
|
||||
import { defineFeature } from '@videojs/store';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
|
||||
export const volumeFeature = defineFeature<HTMLMediaElement>()({
|
||||
import type { FeatureAvailability, PlayerTarget } from '../../types';
|
||||
|
||||
export const volumeFeature = defineFeature<PlayerTarget>()({
|
||||
state: ({ task }) => ({
|
||||
/** Volume level from 0 (silent) to 1 (max). */
|
||||
volume: 1,
|
||||
/** Whether audio is muted. */
|
||||
muted: false,
|
||||
/** Whether volume can be programmatically set on this platform. */
|
||||
volumeAvailability: 'unavailable' as FeatureAvailability,
|
||||
|
||||
/** Set volume (clamped 0-1). Returns the clamped value. */
|
||||
changeVolume(volume: number) {
|
||||
return task({
|
||||
key: 'volume',
|
||||
handler({ target }) {
|
||||
target.volume = Math.max(0, Math.min(1, volume));
|
||||
return target.volume;
|
||||
target.media.volume = Math.max(0, Math.min(1, volume));
|
||||
return target.media.volume;
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -26,20 +30,35 @@ export const volumeFeature = defineFeature<HTMLMediaElement>()({
|
||||
return task({
|
||||
key: 'mute',
|
||||
handler({ target }) {
|
||||
target.muted = !target.muted;
|
||||
return target.muted;
|
||||
target.media.muted = !target.media.muted;
|
||||
return target.media.muted;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const sync = () => set({ volume: target.volume, muted: target.muted });
|
||||
const { media } = target;
|
||||
|
||||
set({ volumeAvailability: canSetVolume() });
|
||||
|
||||
const sync = () => set({ volume: media.volume, muted: media.muted });
|
||||
|
||||
sync();
|
||||
|
||||
listen(target, 'volumechange', sync, { signal });
|
||||
listen(media, 'volumechange', sync, { signal });
|
||||
},
|
||||
});
|
||||
|
||||
export type VolumeState = InferFeatureState<typeof volumeFeature>;
|
||||
|
||||
/** Check if volume can be programmatically set (fails on iOS Safari). */
|
||||
function canSetVolume(): FeatureAvailability {
|
||||
const video = document.createElement('video');
|
||||
try {
|
||||
video.volume = 0.5;
|
||||
return video.volume === 0.5 ? 'available' : 'unsupported';
|
||||
} catch {
|
||||
return 'unsupported';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createFeatureSelector } from '@videojs/store';
|
||||
|
||||
import { bufferFeature } from './features/buffer';
|
||||
import { playbackFeature } from './features/playback';
|
||||
import { sourceFeature } from './features/source';
|
||||
import { timeFeature } from './features/time';
|
||||
import { volumeFeature } from './features/volume';
|
||||
|
||||
export const selectBuffer = createFeatureSelector(bufferFeature);
|
||||
export const selectPlayback = createFeatureSelector(playbackFeature);
|
||||
export const selectSource = createFeatureSelector(sourceFeature);
|
||||
export const selectTime = createFeatureSelector(timeFeature);
|
||||
export const selectVolume = createFeatureSelector(volumeFeature);
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface Media extends HTMLMediaElement {}
|
||||
|
||||
export interface MediaContainer extends HTMLElement {}
|
||||
|
||||
export interface PlayerTarget {
|
||||
media: Media;
|
||||
container: MediaContainer | null;
|
||||
}
|
||||
|
||||
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
|
||||
Reference in New Issue
Block a user