UNPKG

@wmik/use-media-recorder

Version:

React based hooks to utilize the media recorder api for audio, video and screen recording

321 lines (276 loc) 8.45 kB
const React = require('react'); /** * Checks whether the argument is a valid object i.e (key-value pair). * @param {any} o */ function isObject(o) { return o && !Array.isArray(o) && Object(o) === o; } /** * Checks whether media type(audio/video) constraints are valid. * @param {MediaStreamConstraints} mediaType */ function validateMediaTrackConstraints(mediaType) { let supportedMediaConstraints = null; if (navigator.mediaDevices) { supportedMediaConstraints = navigator.mediaDevices.getSupportedConstraints(); } if (supportedMediaConstraints === null) { return; } let unSupportedMediaConstraints = Object.keys(mediaType).filter( constraint => !supportedMediaConstraints[constraint] ); if (unSupportedMediaConstraints.length !== 0) { let toText = unSupportedMediaConstraints.join(','); console.error( `The following constraints ${toText} are not supported on this browser.` ); } } const noop = () => {}; /** * @callback Callback * @param {Blob} blob * * @callback ErrorCallback * @param {Error} error * * @typedef MediaRecorderProps * @type {Object} * @property {BlobPropertyBag} [blobOptions] * @property {Boolean} [recordScreen] * @property {MediaStream} [customMediaStream] * @property {Function} [onStart] * @property {Callback} [onStop] * @property {Callback} [onDataAvailable] * @property {ErrorCallback} [onError] * @property {Object} [mediaRecorderOptions] * @property {MediaStreamConstraints} mediaStreamConstraints * * @typedef MediaRecorderHookOptions * @type {Object} * @property {?Error} error * @property {('idle'|'acquiring_media'|'ready'|'recording'|'paused'|'stopping'|'stopped'|'failed')} status * @property {?Blob} mediaBlob * @property {Boolean} isAudioMuted * @property {Function} stopRecording, * @property {Function} getMediaStream, * @property {Function} clearMediaStream, * @property {Function} clearMediaBlob, * @property {Function} startRecording, * @property {Function} pauseRecording, * @property {Function} resumeRecording, * @property {Function} muteAudio * @property {Function} unMuteAudio * @property {?MediaStream} liveStream * * Creates a custom media recorder object using the MediaRecorder API. * @param {MediaRecorderProps} * @returns {MediaRecorderHookOptions} */ function useMediaRecorder({ blobOptions, recordScreen, customMediaStream, onStop = noop, onStart = noop, onError = noop, mediaRecorderOptions, onDataAvailable = noop, mediaStreamConstraints = {} }) { let mediaChunks = React.useRef([]); let mediaStream = React.useRef(null); let mediaRecorder = React.useRef(null); let [status, setStatus] = React.useState('idle'); let [errorCache, cacheError] = React.useState(null); let [mediaBlobCache, cacheMediaBlob] = React.useState(null); let [isAudioMutedCache, cacheIsAudioMuted] = React.useState(false); async function getMediaStream() { if (errorCache) { cacheError(null); } setStatus('acquiring_media'); if (customMediaStream && customMediaStream instanceof MediaStream) { mediaStream.current = customMediaStream; return customMediaStream; } try { let stream; if (recordScreen) { stream = await window.navigator.mediaDevices.getDisplayMedia( mediaStreamConstraints ); } else { stream = await window.navigator.mediaDevices.getUserMedia( mediaStreamConstraints ); } if (recordScreen && mediaStreamConstraints.audio) { let audioStream = await window.navigator.mediaDevices.getUserMedia({ audio: mediaStreamConstraints.audio }); audioStream .getAudioTracks() .forEach(audioTrack => stream.addTrack(audioTrack)); } mediaStream.current = stream; setStatus('ready'); return stream; } catch (err) { cacheError(err); setStatus('failed'); } } function clearMediaStream() { if (mediaStream.current) { mediaStream.current.getTracks().forEach(track => track.stop()); mediaStream.current = null; setStatus("idle"); } } async function startRecording(timeSlice) { if (errorCache) { cacheError(null); } if (!mediaStream.current) { await getMediaStream(); } mediaChunks.current = []; if (mediaStream.current) { mediaRecorder.current = new MediaRecorder( mediaStream.current, mediaRecorderOptions ); mediaRecorder.current.addEventListener( 'dataavailable', handleDataAvailable ); mediaRecorder.current.addEventListener('stop', handleStop); mediaRecorder.current.addEventListener('error', handleError); try { mediaRecorder.current.start(timeSlice); setStatus('recording'); onStart(); } catch (error) { handleError({ error }); } } } function handleDataAvailable(e) { if (e.data.size) { mediaChunks.current.push(e.data); } onDataAvailable(e.data); } function handleStop() { let blob = new Blob(); let sampleChunk = new Blob(); if (mediaChunks.current.length) { [sampleChunk] = mediaChunks.current; let blobPropertyBag = Object.assign( { type: sampleChunk.type }, blobOptions ); blob = new Blob(mediaChunks.current, blobPropertyBag); cacheMediaBlob(blob); } setStatus('stopped'); onStop(blob); } function handleError(e) { cacheError(e.error); setStatus('idle'); onError(e.error); } function muteAudio(mute) { cacheIsAudioMuted(mute); if (mediaStream.current) { mediaStream.current.getAudioTracks().forEach(audioTrack => { audioTrack.enabled = !mute; }); } } function pauseRecording() { if (mediaRecorder.current && mediaRecorder.current.state === 'recording') { mediaRecorder.current.pause(); setStatus('paused'); } } function resumeRecording() { if (mediaRecorder.current && mediaRecorder.current.state === 'paused') { mediaRecorder.current.resume(); setStatus('recording'); } } function stopRecording() { if (mediaRecorder.current) { setStatus('stopping'); mediaRecorder.current.stop(); // not sure whether to place clean up in useEffect? // If placed in useEffect the handler functions become dependencies of useEffect mediaRecorder.current.removeEventListener( 'dataavailable', handleDataAvailable ); mediaRecorder.current.removeEventListener('stop', handleStop); mediaRecorder.current.removeEventListener('error', handleError); mediaRecorder.current = null; if (!customMediaStream) { clearMediaStream(); } } } function clearMediaBlob() { cacheMediaBlob(null); } React.useEffect(() => { if (!window.MediaRecorder) { throw new ReferenceError( 'MediaRecorder is not supported in this browser. Please ensure that you are running the latest version of chrome/firefox/edge.' ); } if (recordScreen && !window.navigator.mediaDevices.getDisplayMedia) { throw new ReferenceError( 'This browser does not support screen capturing.' ); } if (isObject(mediaStreamConstraints.video)) { validateMediaTrackConstraints(mediaStreamConstraints.video); } if (isObject(mediaStreamConstraints.audio)) { validateMediaTrackConstraints(mediaStreamConstraints.audio); } if (mediaRecorderOptions && mediaRecorderOptions.mimeType) { if (!MediaRecorder.isTypeSupported(mediaRecorderOptions.mimeType)) { console.error( `The specified MIME type supplied to MediaRecorder is not supported by this browser.` ); } } }, [mediaStreamConstraints, mediaRecorderOptions, recordScreen]); return { error: errorCache, status, mediaBlob: mediaBlobCache, isAudioMuted: isAudioMutedCache, stopRecording, getMediaStream, startRecording, pauseRecording, resumeRecording, clearMediaStream, clearMediaBlob, muteAudio: () => muteAudio(true), unMuteAudio: () => muteAudio(false), get liveStream() { if (mediaStream.current) { return new MediaStream(mediaStream.current.getTracks()); } return null; } }; } module.exports = useMediaRecorder;