I've been trying to write a React Hook for handling steaming video captured from a users camera to an HTML Video Element. I'm having trouble working out the best way to handle initialising and de-initialising the camera and HTML Video Element.
I have attempted to add a cleanup function at the end of my hook, but my attempts have ended up with the video re-initialising repeatedly or any number of other weird bugs.
Really I'm struggling to work out how and why the cleanup function is being called. It doesn't seem to relate to the component being unmounted.
Also, I'm unsure how best to destroy the video, though there are plenty of answers for that on here already, I'm not sure I need to remove it entirely. It wouldn't hurt if it hung around, there are only half a dozen pages. I suppose I just want to stop the camera streaming when a user navigates away from the page and start it up again when they return to the video page.
Camera Video Stream Hook
import { useEffect, useState } from 'react';
const initialiseCamera = async() => await
navigator
.mediaDevices
.getUserMedia({audio: false, video: true});
export const useCamera = videoRef => {
const [isCameraInitialised, setIsCameraInitialised] = useState(false);
const [video, setVideo] = useState(null);
const [error, setError] = useState('');
const [playing, setPlaying] = useState(true);
useEffect(() => {
if(video || !videoRef.current) {
return;
}
const videoElement = videoRef.current;
if(videoElement instanceof HTMLVideoElement) {
setVideo(videoRef.current);
}
}, [videoRef, video]);
useEffect(() => {
if(!video || isCameraInitialised || !playing) {
return;
}
initialiseCamera()
.then(stream => {
video.srcObject = stream;
setIsCameraInitialised(true);
})
.catch(e => {
setError(e.message);
setPlaying(false);
});
}, [video, isCameraInitialised, playing]);
useEffect(() => {
const videoElement = videoRef.current;
if(playing) {
videoElement.play();
} else {
videoElement.pause();
}
},[playing, videoRef]);
return [video, isCameraInitialised, playing, setPlaying, error];
};
Video View
import React, {createRef} from 'react';
import { useCamera } from '../hooks/use-camera';
import { Button } from '@orderandchaos/react-components';
const VideoViewDemo = () => {
const videoRef = createRef();
const [video, isCameraInitialised, running, setPlaying, error] = useCamera(videoRef);
return (
<div>
<video
ref={videoRef}
autoPlay={true}
muted={true}
controls
width={480}
height={270}
/>
<Button
onClick={() => setPlaying(!running)}
ariaLabel='Start/Stop Audio'
>{running ? 'Stop' : 'Start'}</Button>
</div>
);
};
export default VideoViewDemo;
If you add a cleanup function within any of the useEffect which has parameters specified as a dependency array, the cleanup function will be run whenever any of the parameters change.
In order for the video cleanup to only run on unmount, you would have to pass an empty dependency array. Now since the variables inside the effect will belong to the closure at the initial run, you would need to have a ref that references those values.
You can write a cleanup hook to take care of that
const useCleanup = (val) => {
const valRef = useRef(val);
useEffect(() => {
valRef.current = val;
}, [val])
useEffect(() => {
return () => {
// cleanup based on valRef.current
}
}, [])
}
import { useEffect, useState } from 'react';
const initialiseCamera = async() => await
navigator
.mediaDevices
.getUserMedia({audio: false, video: true});
export const useCamera = videoRef => {
const [isCameraInitialised, setIsCameraInitialised] = useState(false);
const [video, setVideo] = useState(null);
const [error, setError] = useState('');
const [playing, setPlaying] = useState(true);
useEffect(() => {
if(video || !videoRef.current) {
return;
}
const videoElement = videoRef.current;
if(videoElement instanceof HTMLVideoElement) {
setVideo(videoRef.current);
}
}, [videoRef, video]);
useCleanup(video)
useEffect(() => {
if(!video || isCameraInitialised || !playing) {
return;
}
initialiseCamera()
.then(stream => {
video.srcObject = stream;
setIsCameraInitialised(true);
})
.catch(e => {
setError(e.message);
setPlaying(false);
});
}, [video, isCameraInitialised, playing]);
useEffect(() => {
const videoElement = videoRef.current;
if(playing) {
videoElement.play();
} else {
videoElement.pause();
}
},[playing, videoRef]);
return [video, isCameraInitialised, playing, setPlaying, error];
};