Skip to content

Commit f3bad7a

Browse files
committed
main 🧊 add new hooks for video and audio
1 parent da2ddbf commit f3bad7a

File tree

17 files changed

+1167
-28
lines changed

17 files changed

+1167
-28
lines changed

‎packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@
7272
"@types/react-dom": "^19.1.6",
7373
"@types/web-bluetooth": "^0.0.21",
7474
"@vitejs/plugin-react": "^4.6.0",
75-
"core-js": "^3.43.0",
75+
"core-js": "^3.44.0",
7676
"react": "^19.1.0",
7777
"react-dom": "^19.1.0",
7878
"shx": "^0.4.0",
79-
"vite": "^7.0.2",
79+
"vite": "^7.0.4",
8080
"vite-plugin-dts": "^4.5.4",
8181
"vitest": "^3.2.4"
8282
},

‎packages/core/src/bundle/hooks/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './useActiveElement/useActiveElement';
22
export * from './useAsync/useAsync';
3+
export * from './useAudio/useAudio';
34
export * from './useBattery/useBattery';
45
export * from './useBluetooth/useBluetooth';
56
export * from './useBoolean/useBoolean';
@@ -65,6 +66,7 @@ export * from './useLogger/useLogger';
6566
export * from './useLongPress/useLongPress';
6667
export * from './useMap/useMap';
6768
export * from './useMeasure/useMeasure';
69+
export * from './useMediaControls/useMediaControls';
6870
export * from './useMediaQuery/useMediaQuery';
6971
export * from './useMemory/useMemory';
7072
export * from './useMount/useMount';
@@ -84,6 +86,7 @@ export * from './usePaint/usePaint';
8486
export * from './useParallax/useParallax';
8587
export * from './usePerformanceObserver/usePerformanceObserver';
8688
export * from './usePermission/usePermission';
89+
export * from './usePictureInPicture/usePictureInPicture';
8790
export * from './usePointerLock/usePointerLock';
8891
export * from './usePostMessage/usePostMessage';
8992
export * from './usePreferredColorScheme/usePreferredColorScheme';
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
/**
3+
* @name useAudio
4+
* @description - Hook that manages audio playback with sprite support
5+
* @category Browser
6+
*
7+
* @browserapi Audio https://developer.mozilla.org/en-US/docs/Web/API/Audio
8+
*
9+
* @template Value The type of the value
10+
* @param {string} url The URL of the audio file to play
11+
* @param {UseAudioOptions} [options] Audio configuration options
12+
* @param {number} [options.volume=1] Initial volume level (0 to 1)
13+
* @param {number} [options.playbackRate=1] Initial playback speed (0.5 to 2)
14+
* @param {boolean} [options.interrupt=false] Whether to stop current playback when starting a new one
15+
* @param {boolean} [options.soundEnabled=true] Whether audio playback is initially enabled
16+
* @param {SpriteMap} [options.sprite] Map of named audio segments for sprite-based playback
17+
* @returns {UseAudioReturn} An object containing audio controls and state
18+
*
19+
* @example
20+
* const audio = useAudio("/path/to/sound.mp3");
21+
*/
22+
export const useAudio = (src, options = {}) => {
23+
const [playing, setPlaying] = useState(false);
24+
const [volume, setCurrentVolume] = useState(options.volume ?? 1);
25+
const [playbackRate, setPlaybackRate] = useState(options.playbackRate ?? 1);
26+
const audioRef = useRef(null);
27+
useEffect(() => {
28+
const audio = new Audio(src);
29+
audio.volume = volume;
30+
audio.playbackRate = playbackRate;
31+
audioRef.current = audio;
32+
if (options.immediately) audio.play();
33+
const onPlay = () => setPlaying(true);
34+
const onPause = () => setPlaying(false);
35+
const onEnded = () => setPlaying(false);
36+
const onTimeUpdate = () => {};
37+
const onVolumeChange = () => setCurrentVolume(audio.volume);
38+
const onRateChange = () => setPlaybackRate(audio.playbackRate);
39+
audio.addEventListener('play', onPlay);
40+
audio.addEventListener('pause', onPause);
41+
audio.addEventListener('ended', onEnded);
42+
audio.addEventListener('timeupdate', onTimeUpdate);
43+
audio.addEventListener('volumechange', onVolumeChange);
44+
audio.addEventListener('ratechange', onRateChange);
45+
return () => {
46+
audio.removeEventListener('play', onPlay);
47+
audio.removeEventListener('pause', onPause);
48+
audio.removeEventListener('ended', onEnded);
49+
audio.removeEventListener('timeupdate', onTimeUpdate);
50+
audio.removeEventListener('volumechange', onVolumeChange);
51+
audio.removeEventListener('ratechange', onRateChange);
52+
audio.pause();
53+
audio.remove();
54+
};
55+
}, [src]);
56+
const stop = () => {
57+
if (!audioRef.current) return;
58+
audioRef.current.pause();
59+
audioRef.current.currentTime = 0;
60+
};
61+
const play = async (spriteName) => {
62+
if (!audioRef.current) return;
63+
if (options.interrupt) stop();
64+
if (!spriteName || !options.sprite?.[spriteName]) {
65+
await audioRef.current.play();
66+
return;
67+
}
68+
const [start, end] = options.sprite[spriteName];
69+
audioRef.current.currentTime = start;
70+
await audioRef.current.play();
71+
const checkTime = () => {
72+
if (!audioRef.current) return;
73+
if (audioRef.current.currentTime >= end) {
74+
stop();
75+
}
76+
if (!playing) return;
77+
requestAnimationFrame(checkTime);
78+
};
79+
requestAnimationFrame(checkTime);
80+
};
81+
const pause = () => audioRef.current?.pause();
82+
const setVolume = (value) => {
83+
if (!audioRef.current) return;
84+
const newVolume = Math.max(0, Math.min(1, value));
85+
audioRef.current.volume = newVolume;
86+
setCurrentVolume(newVolume);
87+
};
88+
const changePlaybackRate = (value) => {
89+
if (!audioRef.current) return;
90+
const newRate = Math.max(0.5, Math.min(2, value));
91+
audioRef.current.playbackRate = newRate;
92+
setPlaybackRate(newRate);
93+
};
94+
return {
95+
play,
96+
pause,
97+
stop,
98+
playing,
99+
setVolume,
100+
volume,
101+
changePlaybackRate,
102+
playbackRate
103+
};
104+
};
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { getElement, isTarget } from '@/utils/helpers';
3+
import { useRefState } from '../useRefState/useRefState';
4+
export const timeRangeToArray = (timeRanges) => {
5+
let ranges = [];
6+
for (let i = 0; i < timeRanges.length; ++i)
7+
ranges = [...ranges, [timeRanges.start(i), timeRanges.end(i)]];
8+
return ranges;
9+
};
10+
/**
11+
* @name useMediaControls
12+
* @description Hook that provides controls for HTML media elements (audio/video)
13+
* @category Browser
14+
*
15+
* @overload
16+
* @param {HookTarget} target The target media element
17+
* @param {string} src The source URL of the media
18+
* @returns {UseMediaControlsReturn} An object containing media controls and state
19+
*
20+
* @example
21+
* const { playing, play, pause } = useMediaControls(videoRef, 'video.mp4');
22+
*
23+
* @overload
24+
* @param {HookTarget} target The target media element
25+
* @param {UseMediaSource} options The media source configuration
26+
* @returns {UseMediaControlsReturn} An object containing media controls and state
27+
*
28+
* @example
29+
* const { playing, play, pause } = useMediaControls(audioRef, { src: 'audio.mp3', type: 'audio/mp3' });
30+
*
31+
* @overload
32+
* @template Target The target media element type
33+
* @param {string} src The source URL of the media
34+
* @returns {UseMediaControlsReturn & { ref: StateRef<Target> }} An object containing media controls, state and ref
35+
*
36+
* @example
37+
* const { ref, playing, play, pause } = useMediaControls<HTMLVideoElement>('video.mp4');
38+
*
39+
* @overload
40+
* @template Target The target media element type
41+
* @param {UseMediaSource} options The media source configuration
42+
* @returns {UseMediaControlsReturn & { ref: StateRef<Target> }} An object containing media controls, state and ref
43+
*
44+
* @example
45+
* const { ref, playing, play, pause } = useMediaControls<HTMLVideoElement>({ src: 'video.mp4', type: 'video/mp4' });
46+
*/
47+
export const useMediaControls = (...params) => {
48+
const target = isTarget(params[0]) ? params[0] : undefined;
49+
const options = target
50+
? typeof params[1] === 'object'
51+
? params[1]
52+
: { src: params[1] }
53+
: typeof params[0] === 'object'
54+
? params[0]
55+
: { src: params[0] };
56+
const internalRef = useRefState();
57+
const elementRef = useRef(null);
58+
const [playing, setPlaying] = useState(false);
59+
const [duration, setDuration] = useState(0);
60+
const [currentTime, setCurrentTime] = useState(0);
61+
const [seeking, setSeeking] = useState(false);
62+
const [waiting, setWaiting] = useState(false);
63+
const [buffered, setBuffered] = useState([]);
64+
const [stalled, setStalled] = useState(false);
65+
const [ended, setEnded] = useState(false);
66+
const [playbackRate, setPlaybackRateState] = useState(1);
67+
const [muted, setMutedState] = useState(false);
68+
const [volume, setVolumeState] = useState(1);
69+
useEffect(() => {
70+
const element = target ? getElement(target) : internalRef.current;
71+
if (!element) return;
72+
elementRef.current = element;
73+
element.src = options.src;
74+
if (options.type) element.setAttribute('type', options.type);
75+
if (options.media) element.setAttribute('media', options.media);
76+
setDuration(element.duration);
77+
setCurrentTime(element.currentTime);
78+
setPlaying(false);
79+
setEnded(element.ended);
80+
setMutedState(element.muted);
81+
setVolumeState(element.volume);
82+
setPlaybackRateState(element.playbackRate);
83+
const onPlaying = () => {
84+
setPlaying(true);
85+
setStalled(false);
86+
};
87+
const onPause = () => setPlaying(false);
88+
const onWaiting = () => setWaiting(true);
89+
const onStalled = () => setStalled(true);
90+
const onSeeking = () => setSeeking(true);
91+
const onSeeked = () => setSeeking(false);
92+
const onEnded = () => {
93+
setPlaying(false);
94+
setEnded(true);
95+
};
96+
const onDurationChange = () => setDuration(element.duration);
97+
const onTimeUpdate = () => setCurrentTime(element.currentTime);
98+
const onVolumechange = () => {
99+
setMutedState(element.muted);
100+
setVolumeState(element.volume);
101+
};
102+
const onRatechange = () => setPlaybackRateState(element.playbackRate);
103+
const onProgress = () => setBuffered(timeRangeToArray(element.buffered));
104+
element.addEventListener('playing', onPlaying);
105+
element.addEventListener('pause', onPause);
106+
element.addEventListener('waiting', onWaiting);
107+
element.addEventListener('progress', onProgress);
108+
element.addEventListener('stalled', onStalled);
109+
element.addEventListener('seeking', onSeeking);
110+
element.addEventListener('seeked', onSeeked);
111+
element.addEventListener('ended', onEnded);
112+
element.addEventListener('loadedmetadata', onDurationChange);
113+
element.addEventListener('timeupdate', onTimeUpdate);
114+
element.addEventListener('volumechange', onVolumechange);
115+
element.addEventListener('ratechange', onRatechange);
116+
return () => {
117+
element.removeEventListener('playing', onPlaying);
118+
element.removeEventListener('pause', onPause);
119+
element.removeEventListener('waiting', onWaiting);
120+
element.removeEventListener('progress', onProgress);
121+
element.removeEventListener('stalled', onStalled);
122+
element.removeEventListener('seeking', onSeeking);
123+
element.removeEventListener('seeked', onSeeked);
124+
element.removeEventListener('ended', onEnded);
125+
element.removeEventListener('loadedmetadata', onDurationChange);
126+
element.removeEventListener('timeupdate', onTimeUpdate);
127+
element.removeEventListener('volumechange', onVolumechange);
128+
element.removeEventListener('ratechange', onRatechange);
129+
};
130+
}, [target, internalRef.state]);
131+
const play = async () => {
132+
const element = elementRef.current;
133+
if (!element) return;
134+
await element.play();
135+
};
136+
const pause = () => {
137+
if (!elementRef.current) return;
138+
elementRef.current.pause();
139+
};
140+
const toggle = async () => {
141+
if (playing) return pause();
142+
return play();
143+
};
144+
const seek = (time) => {
145+
if (!elementRef.current) return;
146+
elementRef.current.currentTime = Math.min(Math.max(time, 0), duration);
147+
};
148+
const changeVolume = (value) => {
149+
if (!elementRef.current) return;
150+
elementRef.current.volume = Math.min(Math.max(value, 0), 1);
151+
};
152+
const mute = () => {
153+
if (!elementRef.current) return;
154+
elementRef.current.muted = true;
155+
};
156+
const unmute = () => {
157+
if (!elementRef.current) return;
158+
elementRef.current.muted = false;
159+
};
160+
const changePlaybackRate = (value) => {
161+
if (!elementRef.current) return;
162+
elementRef.current.playbackRate = value;
163+
};
164+
return {
165+
playing,
166+
duration,
167+
currentTime,
168+
seeking,
169+
waiting,
170+
buffered,
171+
stalled,
172+
ended,
173+
playbackRate,
174+
muted,
175+
volume,
176+
play,
177+
pause,
178+
toggle,
179+
seek,
180+
changeVolume,
181+
mute,
182+
unmute,
183+
changePlaybackRate,
184+
...(!target && { ref: internalRef })
185+
};
186+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useState } from 'react';
2+
import { getElement, isTarget } from '@/utils/helpers';
3+
import { useRefState } from '../useRefState/useRefState';
4+
/**
5+
* @name usePictureInPicture
6+
* @description - Hook that provides Picture-in-Picture functionality for video elements
7+
* @category Browser
8+
*
9+
* @browserapi window.PictureInPicture https://developer.mozilla.org/en-US/docs/Web/API/Picture-in-Picture_API
10+
*
11+
* @overload
12+
* @param {HookTarget} target The target video element
13+
* @param {() => void} [options.onEnter] Callback when Picture-in-Picture mode is entered
14+
* @param {() => void} [options.onExit] Callback when Picture-in-Picture mode is exited
15+
* @returns {UsePictureInPictureReturn} An object containing Picture-in-Picture state and controls
16+
*
17+
* @example
18+
* const { open, supported, enter, exit, toggle } = usePictureInPicture(videoRef);
19+
*
20+
* @overload
21+
* @param {() => void} [options.onEnter] Callback when Picture-in-Picture mode is entered
22+
* @param {() => void} [options.onExit] Callback when Picture-in-Picture mode is exited
23+
* @returns {UsePictureInPictureReturn & { ref: StateRef<HTMLVideoElement> }} An object containing Picture-in-Picture state, controls and ref
24+
*
25+
* @example
26+
* const { ref, open, supported, enter, exit, toggle } = usePictureInPicture();
27+
*/
28+
export const usePictureInPicture = (...params) => {
29+
const target = isTarget(params[0]) ? params[0] : undefined;
30+
const options = (target ? params[1] : params[0]) ?? {};
31+
const [open, setOpen] = useState(false);
32+
const internalRef = useRefState();
33+
const supported = typeof document !== 'undefined' && 'pictureInPictureEnabled' in document;
34+
const enter = async () => {
35+
if (!supported) return;
36+
const element = target ? getElement(target) : internalRef.current;
37+
if (!element) return;
38+
await element.requestPictureInPicture();
39+
setOpen(true);
40+
options.onEnter?.();
41+
};
42+
const exit = async () => {
43+
if (!supported) return;
44+
await document.exitPictureInPicture();
45+
setOpen(false);
46+
options.onExit?.();
47+
};
48+
const toggle = async () => {
49+
if (open) await exit();
50+
else await enter();
51+
};
52+
const value = {
53+
open,
54+
supported,
55+
enter,
56+
exit,
57+
toggle
58+
};
59+
if (target) return value;
60+
return { ...value, ref: internalRef };
61+
};

0 commit comments

Comments
 (0)