https://github.com/dcollien/react-media-hooks
Using MediaDevice, WebAudio, etc. in React hooks
https://github.com/dcollien/react-media-hooks
audio audio-processing audiocontext getusermedia getusermedia-access getusermedia-components react react-hooks reactjs streaming video video-processing webaudio webrtc
Last synced: 2 months ago
JSON representation
Using MediaDevice, WebAudio, etc. in React hooks
- Host: GitHub
- URL: https://github.com/dcollien/react-media-hooks
- Owner: dcollien
- Created: 2025-02-25T03:45:48.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-05-20T00:38:06.000Z (about 1 year ago)
- Last Synced: 2025-05-20T01:30:45.769Z (about 1 year ago)
- Topics: audio, audio-processing, audiocontext, getusermedia, getusermedia-access, getusermedia-components, react, react-hooks, reactjs, streaming, video, video-processing, webaudio, webrtc
- Language: TypeScript
- Homepage: https://dcollien.github.io/react-media-hooks/
- Size: 259 KB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# React Hooks for media and audio
A React hook library for media and audio utilities
```
npm install react-media-hooks
```
```typescript
import { useMediaRecorder, useAudioContext } from "react-media-hooks";
// or
import { useMediaRecorder } from "react-media-hooks/use-media";
import { useAudioContext } from "react-media-hooks/use-audio";
```
## Example Usage
```tsx
import { useCallback, useState } from "react";
import {
useAudioContext,
useAudioLevel,
useAudioStreamSource,
} from "react-media-hooks/use-audio";
import {
useMediaBlobRecorder,
useElapsedTime,
useMediaStream,
} from "react-media-hooks/use-media";
import { useBlobUrls } from "react-media-hooks/use-blob";
export function AudioRecorder({
audioDeviceId,
videoDeviceId,
}: {
audioDeviceId: string | null;
videoDeviceId: string | null;
}) {
const [isRecording, setIsRecording] = useState(false);
const constraints =
audioDeviceId && videoDeviceId
? {
audio: { deviceId: { exact: audioDeviceId } },
video: { deviceId: { exact: videoDeviceId } },
}
: null;
// Create the media stream from the constraints
const stream = useMediaStream(constraints);
// Create the media recorder from the stream
const result = useMediaBlobRecorder(stream, isRecording);
// Calculate the time elapsed from the result
const timeElapsed = useElapsedTime(result, isRecording);
// Create the blob urls from the blobs in the result
const blobUrls = useBlobUrls(result.blobs);
// Create the audio source on the audio context, from the stream
const audioSource = useAudioStreamSource(stream);
// Update the audio level (between 0 and 1) from the audio source
// 60 times per second
const { level, timestamp } = useAudioLevel(
audioSource,
1000 / 60 // update 60 times per second
);
// Create the video ref to attach the stream to the video element
const videoRef = useCallback(
(node: HTMLVideoElement) => {
if (node) node.srcObject = stream;
},
[stream]
);
const minutes = timeElapsed.minutes.toFixed(0).padStart(2, "0");
const seconds = timeElapsed.seconds.toFixed(0).padStart(2, "0");
const milliseconds = timeElapsed.millis.toFixed(0).padStart(3, "0");
return (
setIsRecording(true)}>Record
setIsRecording(false)}>Stop
Audio level:
{minutes}:{seconds}.{milliseconds}
Changing the audioDeviceId or videoDeviceId will create a new file
below. Stopping/Starting the recording will clear all files and start a
new recording.
{blobUrls.map((url) => (
))}
);
}
```
## Demo App
```
pnpm run dev
```
## Hooks
### Audio
Hooks for WebAudio
```typescript
import {...} from 'react-media-hooks/use-audio`
```
#### useAudioContext
```typescript
useAudioContext(): AudioContext | null
```
A global audio context is used by default. As per MDN:
> It's recommended to create one AudioContext and reuse it instead of initializing a new one each time, and it's OK to use a single AudioContext for several different audio sources and pipeline concurrently.
Browser vendors decided that Web Audio contexts should not be allowed to automatically play audio; they should instead be started by a user. As such, the `useAudioContext` hook waits for any user interaction before initializing the `AudioContext` so that the context does not get created in suspended mode.
The context is stored as component state so component renders will be triggered when the context initializes and changes from `null` to the initialized `AudioContext` object.
e.g.
```tsx
import { useAudioContext } from "react-media-hooks/use-audio";
function Component() {
const context = useAudioContext(); // will use the global audio context
return <>>;
}
```
Optionally, a React Context Provider can be used to create separate audio contexts. e.g.
```tsx
import { useAudioContext } from "react-media-hooks/use-audio";
import { AudioContextProvider } from "react-media-hooks/audio-context";
function SubComponent() {
const context = useAudioContext(); // will use the context provider's AudioContext
const deviceId = ...;
useEffect(() => {
// Change only the context provider's sink
context.setSinkId(deviceId);
}, [deviceId]);
return <>>;
}
function Component() {
// Create a new AudioContext and initialize it on user interaction
return (
);
}
```
Optionally, your own audio context can be given:
```tsx
function Component() {
// Create a new AudioContext and initialize it on user interaction
return (
);
}
```
If the given context is suspended, it will be automatically woken up on next user interaction.
#### useAudioStreamSource
```typescript
useAudioStreamSource(
stream: MediaStream | null
): MediaStreamAudioSourceNode`
```
Given a MediaStream, creates a MediaStreamSourceNode using `audioContext.createMediaStreamSource(stream)` when both `audioContext` and `stream` become initialized.
The returned source node is stored as component state so component renders will be triggered when the audio source node becomes available.
#### useAudioAnalyser
```typescript
useAudioAnalyser(
source: MediaStreamAudioSourceNode | null,
fftSize = 256
): AnalyserNode | null
```
Given a MediaStreamAudioSourceNode, returns an AnalyserNode. When the `source` changes, the old source will disconnect the analyser (if it exists), and the new source will be connected to the analyser.
#### useAudioLevel
```typescript
useAudioLevel(
source: MediaStreamAudioSourceNode | null,
updateInterval: number | null = 1000 / 60
): {
level: number,
timestamp: number
}
```
Given a MediaStreamAudioSourceNode, returns the current volume level and the timestamp that the volume was last sampled (uses `Date.now()`).
The `updateInterval` controls how frequently the volume level is sampled, and can be set to `null` to pause updates.
#### useAudioDataSource
```typescript
useAudioDataSource(
data: ArrayBuffer | AudioBuffer | Blob | null,
detune?: number,
loop?: boolean,
loopStart?: number,
loopEnd?: number,
playbackRate?: number
): AudioBufferSourceNode | null
```
Returns a AudioBufferSourceNode (when one has initialized) from given audio data: either ArrayBuffer, AudioBuffer, or Blob.
#### useAudioDataPlayback
```typescript
useAudioDataPlayback(
data: ArrayBuffer | AudioBuffer | Blob | null,
detune?: number,
loop?: boolean,
loopStart?: number,
loopEnd?: number,
playbackRate?: number
): (when?: number, offset?: number, duration?: number) => Promise
```
Given audio data (ArrayBuffer, AudioBuffer, or Blob), returns a `start` function. When called this function will start playing the audio.
### Media
Hooks for dealing with MediaDevices
```typescript
import {...} from 'react-media-hooks/use-media`
```
#### useMediaStreamInputDevices
```typescript
useMediaStreamInputDevices(
constraints: MediaStreamConstraints | null.
onAddTrack?: (track: MediaStreamTrack) => void,
onRemoveTrack?: (track: MediaStreamTrack) => void,
onEnded?: () => void,
onAudioEnded?: () => void,
onVideoEnded?: () => void,
onTrackMuted?: (
track: MediaStreamTrack,
mutedTracks: MediaStreamTrack[]
) => void,
onTrackUnmuted?: (
track: MediaStreamTrack,
mutedTracks: MediaStreamTrack[]
) => void
): {
stream: MediaStream | null;
audioDevices: MediaDeviceInfo[];
videoDevices: MediaDeviceInfo[];
reload: () => void;
}
```
Given MediaStreamConstraints, returns a MediaStream and two lists of device info for audio and video respectively. When the constraints change (either the object reference, the value of its `audio` or `video` properties, or their respective `deviceId` properties), then the stream will be replaced with a new MediaStream for that set of constraints.
`useMediaStreamInputDevices` will request permission to use the available devices and update the stream and device lists when permission is granted. If a new device is connected, the lists will update.
Setting `constraints` to `null` will stop the stream.
Calling `reload` will re-initialize the stream and device lists (same as setting constraints to `null` and back to its original value).
The same event handlers can be given as `useMediaStreamWithEvents`.
#### useMediaInputDevicesRequest
```typescript
useMediaInputDevicesRequest(constraints?: {
audio: boolean;
video: boolean;
}): {
audioDevices: MediaDeviceInfo[];
videoDevices: MediaDeviceInfo[];
}
```
Checks whether permissions have already been given, and if not it opens a stream only momentarily, enough to trigger a permissions request and enumerate audio and video devices. The stream is then closed.
Changing the constraints will re-request the devices. If a new device is connected, the lists will update.
#### useMediaPermissionsQuery
```typescript
useMediaPermissionsQuery(): {
microphone: PermissionState | "unsupported" | null;
camera: PermissionState | "unsupported" | null;
}
```
Queries the permissions available.
`microphone` and `camera` will switch from `null` to one of:
- "granted": Permission has already been given
- "denied": Permission was explicitly denied
- "prompt": Permission has not been requested
- "unsupported": The browser doesn't support querying for permissions
#### useMediaDevices
```typescript
useMediaDevices(isPermissionGranted?: boolean): {
readonly reload: () => void;
readonly audioInput: MediaDeviceInfo[];
readonly videoInput: MediaDeviceInfo[];
readonly audioOutput: MediaDeviceInfo[];
}
```
Given a flag for if permission has been granted, returns three lists of media devices: audio inputs, video inputs, and audio outputs. Does not request permissions itself.
Changing the `isPermissionGranted` flag will re-initialize the lists, as will calling `reload()`;
New lists will be automatically set when a new device is made available.
#### useMediaInputDevices
```typescript
useMediaInputDevices(
isPermissionGranted: boolean
): readonly [MediaDeviceInfo[], MediaDeviceInfo[]]
```
Given a flag for if permission has been granted, returns two lists of media input devices: audio and video (in order). Does not request permissions itself.
Changing the `isPermissionGranted` flag will re-initialize the lists.
New lists will be automatically set when a new device is made available.
#### useMediaStream
```typescript
useMediaStream(
constraints: MediaStreamConstraints | null
): MediaStream
```
`useMediaStream` can be used to initialize the media stream (or request permissions) for a set of constraints, separately.
Setting `constraints` to `null` will stop the stream.
#### useMediaStreamWithEvents
```typescript
useMediaStreamWithEvents(
constraints: MediaStreamConstraints | null,
onAddTrack?: (track: MediaStreamTrack) => void,
onRemoveTrack?: (track: MediaStreamTrack) => void,
onEnded?: () => void,
onAudioEnded?: () => void,
onVideoEnded?: () => void,
onTrackMuted?: (
track: MediaStreamTrack,
mutedTracks: MediaStreamTrack[]
) => void,
onTrackUnmuted?: (
track: MediaStreamTrack,
mutedTracks: MediaStreamTrack[]
) => void
): {
stream: MediaStream | null;
reload: () => void;
stopAudio: () => void;
stopVideo: () => void;
stopTrack: (trackId: string) => void;
}
```
Similar to `useMediaStream` but with more options. Calling `reload` will re-initialize the stream.
Calling `stopAudio` will stop all audio tracks, `stopVideo` will stop all video tracks, `stopTrack` to specify a track ID to stop.
Event handlers:
- onAddTrack: when a track is added to the stream
- onRemoveTrack: when a track is removed from the stram
- onEnded: when all tracks are in `readyState = "ended"`
- onAudioEnded: when all audio tracks are in `readyState = "ended"`
- onVideoEnded: when all video tracks are in `readyState = "ended"`
- onTrackMuted: when a track becomes muted
- onTrackUnmuted: when a track becomes unmuted
#### useMediaBlobRecorder
```typescript
useMediaBlobRecorder(
stream: MediaStream | null,
isRecording: boolean,
options?: MediaRecorderOptions
): {
startTime: number | null;
blobs: Blob[];
} as RecordedMediaResult
```
Start recording on a stream. Toggle `isRecording` to start/stop recording. Starting a new recording will re-initialize the `blobs` array. Stopping recording will populate the `blobs` array with new data.
If a stream changes during recording, a new audio file Blob will be added to the `blobs` output array.
The `startTime` is the ms since the `timeOrigin`, i.e. compare against `performance.now()`.
```typescript
useElapsedTime(
result: RecordedMediaResult,
isRecording: boolean,
updateInterval?: number | null
): {
elapsed: number;
minutes: number;
seconds: number;
millis: number;
}
```
Can be used as a shortcut to retrieve the elapsed time since a result was created, updating every `updateInterval`.
### useMediaRecorder
```typescript
useMediaRecorder(
stream: MediaStream | null,
isRecording: boolean,
callbacks?: {
onDataAvailable?: (event: BlobEvent) => void,
onStart?: (event: MediaRecorderEvent, isResuming: boolean) => void,
onStop?: (event: MediaRecorderEvent) => void,
},
options?: UseMediaRecorderOptions
): void
```
Start recording on a stream. Toggle `isRecording` to start/stop recording.
Stream changes: if the stream changes mid-recording, `onStop` will be called, then `onStart` will be called with `isResuming = true` when the new stream starts.
`onDataAvailable` sends `event.data` that can be combined into an audio file `onStop` using:
```typescript
const blob = new Blob([data0, data1, ...], {
type: event.recorder.mimeType,
});
```
`UseMediaRecorderOptions` has a `timeSlice` property which controls how many ms are recorded until `onDataAvailable` is called.
### Utility Hooks used in the Demo App
#### useInterval
```typescript
import { useInterval } from "react-media-hooks/use-interval";
```
```typescript
useInterval(callback: () => void, delay: number | null): void
```
Calls `callback` every `delay` ms.
Setting `delay` to `null` will stop it.
#### useBlobUrls
```typescript
import { useBlobUrls, downloadBlobs } from "react-media-hooks/use-blob";
```
```typescript
useBlobUrls(blobs: Blob[]): string[]
```
Create blob urls for each blob whenever the `blobs` array changes. Revokes old URLs.
Also utility function:
```typescript
downloadBlobs(blobs: Blob[], filePrefix?: string): void
```
Which triggers downloads of an array of blobs.