I'm working on a higher order component that will provide the ability to capture media using the MediaRecorder API. However, when I try to use the captured video (in the form of a Blob passed to createObjectURL) I am getting an error ERR_REQUEST_RANGE_NOT_SATISFIABLE
. When I console log the Blob that is passed to the wrapped component, it has a length of 0. I have included my code at the end of this post.
In order to diagnose the problem, I tried the following tests:
handleDataAvailable
logs the correct value (i.e. [Blob]
).React.useEffect(() => console.log(chunks), [chunks]);
in order to see if chunks is actually getting updated. This also results in the correct value being logged (i.e. [Blob]
).React.useEffect(() => console.log(captured), [captured]);
in order to see if captured is getting updated. This results in a Blob of size 0 being logged.handleStop
, I console log chunks and the blob created by combining the chunks. That results in an empty array and a blob with size 0 respectively.This leads me to believe that handleDataAvailable
is correctly adding each chunk to the chunks array, but somehow the array is being emptied by the time that handleStop
gets run.
Does anyone see what might be causing that to happen?
Code:
import React from 'react';
import { getUserMedia, getConstraints } from '../../utils/general';
const withMediaCapture = (WrappedComponent, recordingType, facingMode, deviceID) => {
const constraints = getConstraints(recordingType, facingMode, deviceID);
const type = recordingType === 'audio'
? 'audio/ogg; codecs=opus'
: 'video/webm; codecs=vp9';
return props => {
const [mediaStream, setMediaStream] = React.useState(undefined);
const [mediaRecorder, setMediaRecorder] = React.useState(undefined);
const [isRecording, setIsRecording] = React.useState(false);
const [captured, setCaptured] = React.useState(undefined);
const [chunks, setChunks] = React.useState([]);
// On mount, get the mediaStream:
const setupStream = () => {
getUserMedia(constraints)
.then(setMediaStream)
.catch(error => {/* TODO: Handle error */});
};
React.useEffect(setupStream, []);
// Once we have gotten the mediaStream, get the mediaRecorder:
const handleDataAvailable = ({ data }) => {
const newChunks = [...chunks, data];
setChunks(newChunks);
};
const handleStop = foo => {
const blob = new Blob(chunks, { type });
setCaptured(blob);
setChunks([]);
}
const getMediaRecorder = () => {
mediaStream && setMediaRecorder(Object.assign(
new MediaRecorder(mediaStream),
{
ondataavailable: handleDataAvailable,
onstop: handleStop,
},
));
}
React.useEffect(getMediaRecorder, [mediaStream]);
const toggleRecording = () => {
isRecording
? mediaRecorder.stop()
: mediaRecorder.start();
setIsRecording(!isRecording);
};
return <WrappedComponent {...{ preview: mediaStream, captured, toggleRecording, isRecording, ...props }} />;
};
};
const VideoCaptureDemo = ({ preview, captured, toggleRecording, isRecording }) => {
const previewRef = React.useRef(null);
const capturedRef = React.useRef(null);
const setupPreview = () => {
previewRef.current.srcObject = preview;
};
React.useEffect(setupPreview, [preview]);
const setupCaptured = () => {
const url = captured && window.URL.createObjectURL(captured);
capturedRef.current.src = url;
};
React.useEffect(setupCaptured, [captured]);
return (
<div>
<video ref={previewRef} autoPlay={true} muted={true} />
<video ref={capturedRef} controls />
<button onClick={toggleRecording}>
{isRecording ? 'Stop Recording' : 'Start Recording'}
</button>
</div>
);
};
export default withMediaCapture(VideoCaptureDemo, 'videoAndAudio');
handleStop
and handleDataAvailable
are both closing over the initial, empty chunks
array. If handleDataAvailable
is called more than once, earlier chunks will be lost, and handleStop
will always create a Blob from the empty chunks array. Re-renders caused by setChunks
will cause new versions of the handle
methods to be created, but the MediaRecorder will still be using the versions from when the MediaRecorder was created.
You could fix handleDataAvailable
by using functional updates, but in order to fix handleStop
I think you would be best off to switch to using useReducer (with the reducer managing both chunks
and captured
) so that you can just dispatch an action and then the reducer can have access to the current chunks and create the Blob appropriately.