-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
316 additions
and
175 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# `useVideo` | ||
|
||
Creates `<video>` element, tracks its state and exposes playback controls. | ||
|
||
|
||
## Usage | ||
|
||
```jsx | ||
import {useVideo} from 'react-use'; | ||
|
||
const Demo = () => { | ||
const [video, state, controls, ref] = useVideo({ | ||
src: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4', | ||
autoPlay: true, | ||
}); | ||
|
||
return ( | ||
<div> | ||
{video} | ||
<pre>{JSON.stringify(state, null, 2)}</pre> | ||
<button onClick={controls.pause}>Pause</button> | ||
<button onClick={controls.play}>Play</button> | ||
<br/> | ||
<button onClick={controls.mute}>Mute</button> | ||
<button onClick={controls.unmute}>Un-mute</button> | ||
<br/> | ||
<button onClick={() => controls.volume(.1)}>Volume: 10%</button> | ||
<button onClick={() => controls.volume(.5)}>Volume: 50%</button> | ||
<button onClick={() => controls.volume(1)}>Volume: 100%</button> | ||
<br/> | ||
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button> | ||
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button> | ||
</div> | ||
); | ||
}; | ||
``` | ||
|
||
|
||
## Reference | ||
|
||
```ts | ||
const [video, state, controls, ref] = useAudio(props); | ||
``` | ||
|
||
`video` is React's `<video>` element that you have to insert somewhere in your | ||
render tree, for example: | ||
|
||
```jsx | ||
<div>{video}</div> | ||
``` | ||
|
||
`state` tracks the state of the video and has the following shape: | ||
|
||
```json | ||
{ | ||
"buffered": [ | ||
{ | ||
"start": 0, | ||
"end": 425.952625 | ||
} | ||
], | ||
"time": 5.244996, | ||
"duration": 425.952625, | ||
"isPlaying": false, | ||
"muted": false, | ||
"volume": 1 | ||
} | ||
``` | ||
|
||
`controls` is a list collection of methods that allow you to control the | ||
playback of the video, it has the following interface: | ||
|
||
```ts | ||
interface AudioControls { | ||
play: () => Promise<void> | void; | ||
pause: () => void; | ||
mute: () => void; | ||
unmute: () => void; | ||
volume: (volume: number) => void; | ||
seek: (time: number) => void; | ||
} | ||
``` | ||
|
||
`ref` is a React reference to HTML `<video>` element, you can access the element by | ||
`ref.current`, note that it may be `null`. | ||
|
||
And finally, `props` — all props that `<video>` accepts. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as React from 'react'; | ||
import {storiesOf} from '@storybook/react'; | ||
import {useVideo} from '..'; | ||
import ShowDocs from '../util/ShowDocs'; | ||
|
||
const Demo = () => { | ||
const [video, state, controls, ref] = useVideo({ | ||
src: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4', | ||
autoPlay: true, | ||
}); | ||
|
||
return ( | ||
<div> | ||
{video} | ||
<pre>{JSON.stringify(state, null, 2)}</pre> | ||
<button onClick={controls.pause}>Pause</button> | ||
<button onClick={controls.play}>Play</button> | ||
<br/> | ||
<button onClick={controls.mute}>Mute</button> | ||
<button onClick={controls.unmute}>Un-mute</button> | ||
<br/> | ||
<button onClick={() => controls.volume(.1)}>Volume: 10%</button> | ||
<button onClick={() => controls.volume(.5)}>Volume: 50%</button> | ||
<button onClick={() => controls.volume(1)}>Volume: 100%</button> | ||
<br/> | ||
<button onClick={() => controls.seek(state.time - 5)}>-5 sec</button> | ||
<button onClick={() => controls.seek(state.time + 5)}>+5 sec</button> | ||
</div> | ||
); | ||
}; | ||
|
||
storiesOf('useVideo', module) | ||
.add('Docs', () => <ShowDocs md={require('../../docs/useVideo.md')} />) | ||
.add('Demo', () => | ||
<Demo/> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,177 +1,5 @@ | ||
import * as React from 'react'; | ||
import {useEffect, useRef, ReactRef} from './react'; | ||
import useSetState from './useSetState'; | ||
import parseTimeRanges from './util/parseTimeRanges'; | ||
import createHTMLMediaHook from './util/createHTMLMediaHook'; | ||
|
||
export interface AudioProps extends React.AudioHTMLAttributes<any> { | ||
src: string; | ||
} | ||
|
||
export interface AudioState { | ||
buffered: any[]; | ||
duration: number; | ||
isPlaying: boolean; | ||
muted: boolean; | ||
time: number; | ||
volume: number; | ||
} | ||
|
||
export interface AudioControls { | ||
play: () => Promise<void> | void; | ||
pause: () => void; | ||
mute: () => void; | ||
unmute: () => void; | ||
volume: (volume: number) => void; | ||
seek: (time: number) => void; | ||
} | ||
|
||
const useAudio = (props: AudioProps): [React.ReactElement<AudioProps>, AudioState, AudioControls, ReactRef<HTMLAudioElement | null>] => { | ||
const [state, setState] = useSetState<AudioState>({ | ||
buffered: [], | ||
time: 0, | ||
duration: 0, | ||
isPlaying: false, | ||
muted: false, | ||
volume: 1, | ||
}); | ||
const ref = useRef<HTMLAudioElement | null>(null); | ||
|
||
const wrapEvent = (userEvent, proxyEvent?) => { | ||
return (event) => { | ||
try { | ||
proxyEvent && proxyEvent(event); | ||
} finally { | ||
userEvent && userEvent(event); | ||
} | ||
}; | ||
}; | ||
|
||
const onPlay = () => setState({isPlaying: true}); | ||
const onPause = () => setState({isPlaying: false}); | ||
const onVolumeChange = () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
setState({ | ||
muted: el.muted, | ||
volume: el.volume, | ||
}); | ||
}; | ||
const onDurationChange = () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
const {duration, buffered} = el; | ||
setState({ | ||
duration, | ||
buffered: parseTimeRanges(buffered), | ||
}); | ||
}; | ||
const onTimeUpdate = () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
setState({time: el.currentTime}); | ||
}; | ||
const onProgress = () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
setState({buffered: parseTimeRanges(el.buffered)}); | ||
}; | ||
|
||
const element = React.createElement('audio', { | ||
controls: false, | ||
...props, | ||
ref, | ||
onPlay: wrapEvent(props.onPlay, onPlay), | ||
onPause: wrapEvent(props.onPause, onPause), | ||
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange), | ||
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange), | ||
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate), | ||
onProgress: wrapEvent(props.onProgress, onProgress), | ||
}); | ||
|
||
// Some browsers return `Promise` on `.play()` and may throw errors | ||
// if one tries to execute another `.play()` or `.pause()` while that | ||
// promise is resolving. So we prevent that with this lock. | ||
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=593273 | ||
let lockPlay: boolean = false; | ||
|
||
const controls = { | ||
play: () => { | ||
const el = ref.current; | ||
if (!el) return undefined; | ||
|
||
if (!lockPlay) { | ||
const promise = el.play(); | ||
const isPromise = typeof promise === 'object'; | ||
|
||
if (isPromise) { | ||
lockPlay = true; | ||
const resetLock = () => { | ||
lockPlay = false; | ||
}; | ||
promise.then(resetLock, resetLock); | ||
} | ||
|
||
return promise; | ||
} | ||
return undefined; | ||
}, | ||
pause: () => { | ||
const el = ref.current; | ||
if (el && !lockPlay) { | ||
return el.pause(); | ||
} | ||
}, | ||
seek: (time: number) => { | ||
const el = ref.current; | ||
if (!el || (state.duration === undefined)) return; | ||
time = Math.min(state.duration, Math.max(0, time)); | ||
el.currentTime = time; | ||
}, | ||
volume: (volume: number) => { | ||
const el = ref.current; | ||
if (!el) return; | ||
volume = Math.min(1, Math.max(0, volume)); | ||
el.volume = volume; | ||
setState({volume}); | ||
}, | ||
mute: () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
el.muted = true; | ||
}, | ||
unmute: () => { | ||
const el = ref.current; | ||
if (!el) return; | ||
el.muted = false; | ||
}, | ||
}; | ||
|
||
useEffect(() => { | ||
const el = ref.current!; | ||
|
||
if (!el) { | ||
if (process.env.NODE_ENV !== 'production') { | ||
console.error( | ||
'useAudio() ref to <audio> element is empty at mount. ' + | ||
'It seem you have not rendered the audio element, which is ' + | ||
'returns as the first argument const [audio] = useAudio(...).' | ||
); | ||
} | ||
return; | ||
} | ||
|
||
// Start media, if autoPlay requested. | ||
if (props.autoPlay && el.paused) { | ||
controls.play(); | ||
} | ||
|
||
setState({ | ||
volume: el.volume, | ||
muted: el.muted, | ||
}); | ||
}, [props.src]); | ||
|
||
return [element, state, controls, ref]; | ||
}; | ||
const useAudio = createHTMLMediaHook('audio'); | ||
|
||
export default useAudio; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import createHTMLMediaHook from './util/createHTMLMediaHook'; | ||
|
||
const useVideo = createHTMLMediaHook('video'); | ||
|
||
export default useVideo; |
Oops, something went wrong.