Skip to content

Commit 1774994

Browse files
committed
[IMPL] useAudio hook
1 parent a61a4ce commit 1774994

12 files changed

Lines changed: 256 additions & 7 deletions

File tree

3.89 KB
Binary file not shown.

apps/react-tools-demo/src/assets/bubbles.mp3:Zone.Identifier

Whitespace-only changes.

apps/react-tools-demo/src/components/hooks/useAnimation/UseAnimation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useAnimation } from "../../../../../../packages/react-tools/src/hooks"
1+
import { useAnimation } from "../../../../../../packages/react-tools/src";
22

33
/**
44
The component uses _useAnimation_ to compute an animation on p element. Use the buttons to perform action on animation.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useAudio } from '../../../../../../packages/react-tools/src';
2+
import audio from '../../../assets/bubbles.mp3';
3+
4+
/**
5+
The component use _useAudio_ hook to use an audio track.
6+
*/
7+
export const UseAudio = () => {
8+
const { state, play, pause } = useAudio({ url: audio, loop: true });
9+
10+
return <div>
11+
<p>Status: {state.status}</p>
12+
<button onClick={play} disabled={state.status === "playing"}>Play</button>
13+
<button onClick={pause} disabled={state.status !== "playing"}>Pause</button>
14+
</div>
15+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export const COMPONENTS = [
106106
"useDocumentPIP",
107107
"usePopover",
108108
"useRemotePlayback",
109-
"useAnimation"
109+
"useAnimation",
110+
"useAudio"
110111
]
111112
],
112113
//UTILS
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# useAudio
2+
Hook to use an Audio source.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseAudio = () => {
8+
const { state, play, pause } = useAudio({ url: audio });
9+
10+
return <div>
11+
<p>Status: {state.status}</p>
12+
<button onClick={play} disabled={state.status === "playing"}>Play</button>
13+
<button onClick={pause} disabled={state.status !== "playing"}>Pause</button>
14+
</div>
15+
}
16+
```
17+
18+
> The component use _useAudio_ hook to use an audio track.
19+
20+
21+
## API
22+
23+
```tsx
24+
useAudio({ url, volume, loop, defaultMuted, autoPlay, playbackRate, onError }: UseAudioProps): UseAudioResult
25+
```
26+
27+
> ### Params
28+
>
29+
> - __param__: _UseAudioProps_
30+
object
31+
> - __param.url?__: _string_
32+
An optional string containing the URL of an audio file to be associated with the new audio element.
33+
> - __param.volume?__: _number_
34+
A double indicating the audio volume, from 0.0 (silent) to 1.0 (loudest).
35+
> - __param.loop?__: _boolean_
36+
A boolean that reflects the loop HTML attribute, which indicates whether the media element should start over when it reaches the end.
37+
> - __param.defaultMuted?__: _boolean_
38+
A boolean that reflects the muted HTML attribute, which indicates whether the media element's audio output should be muted by default.
39+
> - __param.autoPlay?__: _boolean_
40+
A boolean value that reflects the autoplay HTML attribute, indicating whether playback should automatically begin as soon as enough media is available to do so without interruption.
41+
> - __param.playbackRate?__: _number_
42+
A double that indicates the rate at which the media is being played back.
43+
> - __param.onError] ?__: _OnErrorEventHandler_
44+
>
45+
46+
> ### Returns
47+
>
48+
> __result__: _UseAudioResult_
49+
> Object with these properties:
50+
> - __state__: object with current audio properties:
51+
> - _status_: value between __"unavailable"__ __"ready"__ __"playing"__ __"pause"__ indicating current audio status.
52+
> - _volume_: current audio volume.
53+
> - _playbackRate_: current audio playbackRate.
54+
> - __setAudio__: function to set audio.
55+
> - __setPlaybackRate__: function to set audio playbackRate.
56+
> - __setVolume__: function to set audio volume.
57+
> - __play__: function to play audio.
58+
> - __pause__: function to pause audio.
59+
> - __load__: function to reload audio.
60+
>

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
- [x] usePopover
110110
- [x] useRemotePlayback
111111
- [x] useAnimation
112-
- [ ] useAudio (???)
112+
- [x] useAudio
113113
- [ ] useVideo (???)
114114
- [ ] useEventSource (https://vueuse.org/core/useEventSource/)
115115
- [ ] useWebSocket (https://vueuse.org/core/useWebSocket/)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,5 @@ export { usePIP } from './usePIP';
8686
export { useDocumentPIP } from './useDocumentPIP';
8787
export { usePopover } from './usePopover';
8888
export { useRemotePlayback } from './useRemotePlayback';
89-
export { useAnimation } from './useAnimation';
89+
export { useAnimation } from './useAnimation';
90+
export { useAudio } from './useAudio';
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useCallback, useRef } from "react"
2+
import { useEffectOnce, useSyncExternalStore } from ".";
3+
import { UseAudioProps, UseAudioResult } from "../models";
4+
5+
/**
6+
* **`useAudio`**: Hook to use an Audio source.
7+
* @param {UseAudioProps} param - object
8+
* @param {string} [param.url] - An optional string containing the URL of an audio file to be associated with the new audio element.
9+
* @param {number} [param.volume] - A double indicating the audio volume, from 0.0 (silent) to 1.0 (loudest).
10+
* @param {boolean} [param.loop] - A boolean that reflects the loop HTML attribute, which indicates whether the media element should start over when it reaches the end.
11+
* @param {boolean} [param.defaultMuted] - A boolean that reflects the muted HTML attribute, which indicates whether the media element's audio output should be muted by default.
12+
* @param {number} [param.playbackRate] - A double that indicates the rate at which the media is being played back.
13+
* @param {OnErrorEventHandler} [param.onError] -
14+
* @returns {UseAudioResult} result
15+
* Object with these properties:
16+
* - __state__: object with current audio properties:
17+
* - _status_: value between __"unavailable"__ __"ready"__ __"playing"__ __"pause"__ indicating current audio status.
18+
* - _volume_: current audio volume.
19+
* - _playbackRate_: current audio playbackRate.
20+
* - __setAudio__: function to set audio.
21+
* - __setPlaybackRate__: function to set audio playbackRate.
22+
* - __setVolume__: function to set audio volume.
23+
* - __play__: function to play audio.
24+
* - __pause__: function to pause audio.
25+
* - __load__: function to reload audio.
26+
*/
27+
export const useAudio = ({ url, volume, loop, defaultMuted, playbackRate, onError }: UseAudioProps): UseAudioResult => {
28+
const notifyRef = useRef<() => void>();
29+
const audioRef = useRef<HTMLAudioElement>();
30+
const status = useRef<"unavailable" | "ready" | "playing" | "pause">(!url ? "unavailable" : "ready");
31+
const stateCached = useRef<{ status: "unavailable" | "ready" | "playing" | "pause", volume?: number, playbackRate?: number }>({
32+
status: !url ? "unavailable" : "ready",
33+
playbackRate,
34+
volume
35+
});
36+
37+
const setAudio = useCallback((audio: string, autoPlay?: boolean) => {
38+
audioRef.current && (audioRef.current.pause(), audioRef.current.src = "");
39+
audioRef.current = new Audio(audio);
40+
audioRef.current.autoplay = !!autoPlay;
41+
stateCached.current?.volume && (audioRef.current.volume = stateCached.current.volume);
42+
stateCached.current?.playbackRate && (audioRef.current.playbackRate = stateCached.current.playbackRate);
43+
status.current = "ready";
44+
audioRef.current.onended = () => {
45+
status.current = "ready";
46+
notifyRef.current && notifyRef.current();
47+
}
48+
!!onError && (audioRef.current.onerror = onError);
49+
notifyRef.current && notifyRef.current();
50+
}, [onError]);
51+
52+
const setVolume = useCallback((volume: number) => {
53+
stateCached.current = {
54+
...stateCached.current,
55+
volume
56+
};
57+
audioRef.current && (audioRef.current.volume = volume);
58+
notifyRef.current && notifyRef.current();
59+
}, []);
60+
61+
const setPlaybackRate = useCallback((playbackRate: number) => {
62+
stateCached.current = {
63+
...stateCached.current,
64+
playbackRate
65+
};
66+
audioRef.current && (audioRef.current.playbackRate = playbackRate);
67+
notifyRef.current && notifyRef.current();
68+
}, []);
69+
70+
const play = useCallback(() => {
71+
status.current = "playing";
72+
audioRef.current && audioRef.current.play();
73+
notifyRef.current && notifyRef.current();
74+
}, []);
75+
76+
const pause = useCallback(() => {
77+
status.current = "pause";
78+
audioRef.current && audioRef.current.pause();
79+
notifyRef.current && notifyRef.current();
80+
}, []);
81+
82+
const load = useCallback(() => {
83+
status.current = "ready";
84+
audioRef.current && audioRef.current.load();
85+
notifyRef.current && notifyRef.current();
86+
}, [])
87+
88+
useEffectOnce(() => () => {
89+
audioRef.current && (audioRef.current.src = "");
90+
audioRef.current = undefined;
91+
})
92+
93+
const state = useSyncExternalStore(
94+
useCallback(notif => {
95+
notifyRef.current = notif;
96+
return () => {
97+
notifyRef.current = undefined;
98+
};
99+
}, []),
100+
useCallback(() => {
101+
if (status.current !== stateCached.current.status) {
102+
stateCached.current = {
103+
...stateCached.current,
104+
status: status.current
105+
}
106+
}
107+
if (audioRef.current) {
108+
if (audioRef.current.volume !== stateCached.current.volume || audioRef.current.playbackRate !== stateCached.current.playbackRate) {
109+
stateCached.current = {
110+
...stateCached.current,
111+
volume: audioRef.current.volume,
112+
playbackRate: audioRef.current.playbackRate
113+
}
114+
}
115+
}
116+
return stateCached.current
117+
}, [])
118+
);
119+
120+
if (!audioRef.current && url) {
121+
audioRef.current = new Audio(url);
122+
!!volume && (audioRef.current.volume = volume);
123+
!!playbackRate && (audioRef.current.playbackRate = playbackRate);
124+
!!loop && (audioRef.current.loop = loop);
125+
!!defaultMuted && (audioRef.current.defaultMuted = defaultMuted);
126+
stateCached.current.playbackRate = audioRef.current.playbackRate;
127+
stateCached.current.volume = audioRef.current.volume;
128+
audioRef.current.onended = () => {
129+
status.current = "ready";
130+
notifyRef.current && notifyRef.current();
131+
}
132+
!!onError && (audioRef.current.onerror = onError);
133+
notifyRef.current && notifyRef.current();
134+
}
135+
136+
return {
137+
state,
138+
setAudio,
139+
setPlaybackRate,
140+
setVolume,
141+
play,
142+
pause,
143+
load,
144+
}
145+
}

packages/react-tools/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ export type {
5656
UseRemotePlaybackProps,
5757
UseRemotePlaybackResult,
5858
UseAnimationProps,
59-
UseAnimationResult
59+
UseAnimationResult,
60+
UseAudioProps,
61+
UseAudioResult
6062
} from './models'
6163

6264
export {
@@ -147,7 +149,9 @@ export {
147149
usePIP,
148150
useDocumentPIP,
149151
usePopover,
150-
useRemotePlayback
152+
useRemotePlayback,
153+
useAnimation,
154+
useAudio
151155
} from './hooks'
152156

153157
export {

0 commit comments

Comments
 (0)