Skip to content

Commit ff05419

Browse files
committed
feat: 🎸 add useVideo hook
1 parent 2abf53a commit ff05419

File tree

8 files changed

+316
-175
lines changed

8 files changed

+316
-175
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- [**UI**](./docs/UI.md)
5050
- [`useAudio`](./docs/useAudio.md) — plays audio and exposes its controls. [![][img-demo]](https://codesandbox.io/s/5v7q47knwl)
5151
- [`useSpeech`](./docs/useSpeech.md) — synthesizes speech from a text string. [![][img-demo]](https://codesandbox.io/s/n090mqz69m)
52+
- [`useVideo`](./docs/useVideo.md) — plays video, tracks its state, and exposes playback controls.
5253
<br/>
5354
<br/>
5455
- [**Animations**](./docs/Animations.md)

docs/useAudio.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `useAudio`
22

3-
Creates `<audio>` element, tracks its state and exposes playback conrols.
3+
Creates `<audio>` element, tracks its state and exposes playback controls.
44

55

66
## Usage

docs/useVideo.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# `useVideo`
2+
3+
Creates `<video>` element, tracks its state and exposes playback controls.
4+
5+
6+
## Usage
7+
8+
```jsx
9+
import {useVideo} from 'react-use';
10+
11+
const Demo = () => {
12+
const [video, state, controls, ref] = useVideo({
13+
src: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4',
14+
autoPlay: true,
15+
});
16+
17+
return (
18+
<div>
19+
{video}
20+
<pre>{JSON.stringify(state, null, 2)}</pre>
21+
<button onClick={controls.pause}>Pause</button>
22+
<button onClick={controls.play}>Play</button>
23+
<br/>
24+
<button onClick={controls.mute}>Mute</button>
25+
<button onClick={controls.unmute}>Un-mute</button>
26+
<br/>
27+
<button onClick={() => controls.volume(.1)}>Volume: 10%</button>
28+
<button onClick={() => controls.volume(.5)}>Volume: 50%</button>
29+
<button onClick={() => controls.volume(1)}>Volume: 100%</button>
30+
<br/>
31+
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button>
32+
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button>
33+
</div>
34+
);
35+
};
36+
```
37+
38+
39+
## Reference
40+
41+
```ts
42+
const [video, state, controls, ref] = useAudio(props);
43+
```
44+
45+
`video` is React's `<video>` element that you have to insert somewhere in your
46+
render tree, for example:
47+
48+
```jsx
49+
<div>{video}</div>
50+
```
51+
52+
`state` tracks the state of the video and has the following shape:
53+
54+
```json
55+
{
56+
"buffered": [
57+
{
58+
"start": 0,
59+
"end": 425.952625
60+
}
61+
],
62+
"time": 5.244996,
63+
"duration": 425.952625,
64+
"isPlaying": false,
65+
"muted": false,
66+
"volume": 1
67+
}
68+
```
69+
70+
`controls` is a list collection of methods that allow you to control the
71+
playback of the video, it has the following interface:
72+
73+
```ts
74+
interface AudioControls {
75+
play: () => Promise<void> | void;
76+
pause: () => void;
77+
mute: () => void;
78+
unmute: () => void;
79+
volume: (volume: number) => void;
80+
seek: (time: number) => void;
81+
}
82+
```
83+
84+
`ref` is a React reference to HTML `<video>` element, you can access the element by
85+
`ref.current`, note that it may be `null`.
86+
87+
And finally, `props` &mdash; all props that `<video>` accepts.

src/__stories__/useVideo.story.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as React from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {useVideo} from '..';
4+
import ShowDocs from '../util/ShowDocs';
5+
6+
const Demo = () => {
7+
const [video, state, controls, ref] = useVideo({
8+
src: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4',
9+
autoPlay: true,
10+
});
11+
12+
return (
13+
<div>
14+
{video}
15+
<pre>{JSON.stringify(state, null, 2)}</pre>
16+
<button onClick={controls.pause}>Pause</button>
17+
<button onClick={controls.play}>Play</button>
18+
<br/>
19+
<button onClick={controls.mute}>Mute</button>
20+
<button onClick={controls.unmute}>Un-mute</button>
21+
<br/>
22+
<button onClick={() => controls.volume(.1)}>Volume: 10%</button>
23+
<button onClick={() => controls.volume(.5)}>Volume: 50%</button>
24+
<button onClick={() => controls.volume(1)}>Volume: 100%</button>
25+
<br/>
26+
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button>
27+
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button>
28+
</div>
29+
);
30+
};
31+
32+
storiesOf('useVideo', module)
33+
.add('Docs', () => <ShowDocs md={require('../../docs/useVideo.md')} />)
34+
.add('Demo', () =>
35+
<Demo/>
36+
)

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import useToggle from './useToggle';
3636
import useTween from './useTween';
3737
import useUnmount from './useUnmount';
3838
import useUpdate from './useUpdate';
39+
import useVideo from './useVideo';
3940
import useWindowSize from './useWindowSize';
4041

4142
export {
@@ -77,5 +78,6 @@ export {
7778
useTween,
7879
useUnmount,
7980
useUpdate,
81+
useVideo,
8082
useWindowSize,
8183
};

src/useAudio.ts

Lines changed: 2 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,5 @@
1-
import * as React from 'react';
2-
import {useEffect, useRef, ReactRef} from './react';
3-
import useSetState from './useSetState';
4-
import parseTimeRanges from './util/parseTimeRanges';
1+
import createHTMLMediaHook from './util/createHTMLMediaHook';
52

6-
export interface AudioProps extends React.AudioHTMLAttributes<any> {
7-
src: string;
8-
}
9-
10-
export interface AudioState {
11-
buffered: any[];
12-
duration: number;
13-
isPlaying: boolean;
14-
muted: boolean;
15-
time: number;
16-
volume: number;
17-
}
18-
19-
export interface AudioControls {
20-
play: () => Promise<void> | void;
21-
pause: () => void;
22-
mute: () => void;
23-
unmute: () => void;
24-
volume: (volume: number) => void;
25-
seek: (time: number) => void;
26-
}
27-
28-
const useAudio = (props: AudioProps): [React.ReactElement<AudioProps>, AudioState, AudioControls, ReactRef<HTMLAudioElement | null>] => {
29-
const [state, setState] = useSetState<AudioState>({
30-
buffered: [],
31-
time: 0,
32-
duration: 0,
33-
isPlaying: false,
34-
muted: false,
35-
volume: 1,
36-
});
37-
const ref = useRef<HTMLAudioElement | null>(null);
38-
39-
const wrapEvent = (userEvent, proxyEvent?) => {
40-
return (event) => {
41-
try {
42-
proxyEvent && proxyEvent(event);
43-
} finally {
44-
userEvent && userEvent(event);
45-
}
46-
};
47-
};
48-
49-
const onPlay = () => setState({isPlaying: true});
50-
const onPause = () => setState({isPlaying: false});
51-
const onVolumeChange = () => {
52-
const el = ref.current;
53-
if (!el) return;
54-
setState({
55-
muted: el.muted,
56-
volume: el.volume,
57-
});
58-
};
59-
const onDurationChange = () => {
60-
const el = ref.current;
61-
if (!el) return;
62-
const {duration, buffered} = el;
63-
setState({
64-
duration,
65-
buffered: parseTimeRanges(buffered),
66-
});
67-
};
68-
const onTimeUpdate = () => {
69-
const el = ref.current;
70-
if (!el) return;
71-
setState({time: el.currentTime});
72-
};
73-
const onProgress = () => {
74-
const el = ref.current;
75-
if (!el) return;
76-
setState({buffered: parseTimeRanges(el.buffered)});
77-
};
78-
79-
const element = React.createElement('audio', {
80-
controls: false,
81-
...props,
82-
ref,
83-
onPlay: wrapEvent(props.onPlay, onPlay),
84-
onPause: wrapEvent(props.onPause, onPause),
85-
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
86-
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
87-
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
88-
onProgress: wrapEvent(props.onProgress, onProgress),
89-
});
90-
91-
// Some browsers return `Promise` on `.play()` and may throw errors
92-
// if one tries to execute another `.play()` or `.pause()` while that
93-
// promise is resolving. So we prevent that with this lock.
94-
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=593273
95-
let lockPlay: boolean = false;
96-
97-
const controls = {
98-
play: () => {
99-
const el = ref.current;
100-
if (!el) return undefined;
101-
102-
if (!lockPlay) {
103-
const promise = el.play();
104-
const isPromise = typeof promise === 'object';
105-
106-
if (isPromise) {
107-
lockPlay = true;
108-
const resetLock = () => {
109-
lockPlay = false;
110-
};
111-
promise.then(resetLock, resetLock);
112-
}
113-
114-
return promise;
115-
}
116-
return undefined;
117-
},
118-
pause: () => {
119-
const el = ref.current;
120-
if (el && !lockPlay) {
121-
return el.pause();
122-
}
123-
},
124-
seek: (time: number) => {
125-
const el = ref.current;
126-
if (!el || (state.duration === undefined)) return;
127-
time = Math.min(state.duration, Math.max(0, time));
128-
el.currentTime = time;
129-
},
130-
volume: (volume: number) => {
131-
const el = ref.current;
132-
if (!el) return;
133-
volume = Math.min(1, Math.max(0, volume));
134-
el.volume = volume;
135-
setState({volume});
136-
},
137-
mute: () => {
138-
const el = ref.current;
139-
if (!el) return;
140-
el.muted = true;
141-
},
142-
unmute: () => {
143-
const el = ref.current;
144-
if (!el) return;
145-
el.muted = false;
146-
},
147-
};
148-
149-
useEffect(() => {
150-
const el = ref.current!;
151-
152-
if (!el) {
153-
if (process.env.NODE_ENV !== 'production') {
154-
console.error(
155-
'useAudio() ref to <audio> element is empty at mount. ' +
156-
'It seem you have not rendered the audio element, which is ' +
157-
'returns as the first argument const [audio] = useAudio(...).'
158-
);
159-
}
160-
return;
161-
}
162-
163-
// Start media, if autoPlay requested.
164-
if (props.autoPlay && el.paused) {
165-
controls.play();
166-
}
167-
168-
setState({
169-
volume: el.volume,
170-
muted: el.muted,
171-
});
172-
}, [props.src]);
173-
174-
return [element, state, controls, ref];
175-
};
3+
const useAudio = createHTMLMediaHook('audio');
1764

1775
export default useAudio;

src/useVideo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import createHTMLMediaHook from './util/createHTMLMediaHook';
2+
3+
const useVideo = createHTMLMediaHook('video');
4+
5+
export default useVideo;

0 commit comments

Comments
 (0)