Last active
January 20, 2020 18:26
-
-
Save anderjs/a1cd36b549a3def98a8afbd59edc42ef to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { memo, useEffect, useRef, useState } from 'react' | |
import Icon from 'react-icons-kit' | |
import { Button } from 'react-bootstrap' | |
import { ic_play_arrow } from 'react-icons-kit/md/ic_play_arrow' | |
import { ic_pause } from 'react-icons-kit/md/ic_pause' | |
import { ToastsStore } from 'react-toasts' | |
import FlexContainer from './FlexContainer' | |
import Stopwatch from './Stopwatch' | |
import Emoji from './Emoji' | |
import { TOAST_EXPIRATION } from '../constants' | |
// import Beep from '../assets/audio/beep.mp3' | |
/** | |
* @type {React.FunctionComponent<{ controls?: boolean, disabled?: boolean, displayToast?: boolean, format: 'audio/mpeg' | 'audio/wav', maxDuration?: number, onAudioCapture?: () => Blob, onAudioStart?: () => void}>} | |
*/ | |
const AudioRecorder = ({ | |
controls, | |
disabled, | |
displayToast, | |
format, | |
maxDuration, | |
onAudioCapture, | |
onAudioStart | |
}) => { | |
const [, setStream] = useState(null) | |
const [error, setError] = useState(null) | |
const [recorder, setRecorder] = useState(null) | |
const [recording, setRecording] = useState(null) | |
const audioNodeRef = useRef(null) | |
/** | |
* @description | |
* Injecting the component the component when is mounted. | |
* Answers for the navigator. | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia | |
*/ | |
useEffect(() => { | |
navigator.mediaDevices | |
.getUserMedia({ audio: true, video: false }) | |
.then(handleMediaStream) | |
.catch(handleMediaError) | |
}, []) | |
/** | |
* @description | |
* Handles the Promise of naviagtor.mediaDevices. | |
* @param {MediaStream} stream | |
* @returns {void} void | |
*/ | |
function handleMediaStream(stream) { | |
setStream(stream) | |
setRecorder(new MediaRecorder(stream)) | |
} | |
/** | |
* @description | |
* Catch the error of navigator.mediaDevices. | |
* @param {any} err | |
* @returns {void} void | |
*/ | |
function handleMediaError(err) { | |
setError(err) | |
} | |
/** | |
* @description | |
* This function will record all input from the context of our recorder. | |
* It dispatch a callback to the parent component @function onAudioStart() to indicates that it's been started. | |
* Also, we keep our state safe, with setRecording. | |
* And the end of the record we call @function onAudioCapture() that returns a @type Blob. | |
*/ | |
function handleMediaPlayback() { | |
if (!recording && recorder.state === 'inactive') { | |
displayToast && ToastsStore.info('Grabando respuesta', TOAST_EXPIRATION) | |
onAudioStart() | |
setRecording(true) | |
audioNodeRef.current.pause() | |
if (recorder !== null) { | |
recorder.start() | |
const chunks = [] | |
recorder.ondataavailable = function(e) { | |
chunks.push(e.data) | |
if (recorder.state === 'inactive') { | |
const blob = new Blob(chunks, { type: format }) | |
const recordURI = URL.createObjectURL(blob) | |
audioNodeRef.current.src = recordURI | |
audioNodeRef.current.controls = true | |
const reader = new FileReader() | |
reader.onload = function(e) { | |
onAudioCapture({ | |
url: recordURI, | |
blob, | |
result: e.target.result | |
}) | |
} | |
reader.readAsDataURL(blob) | |
} | |
} | |
} | |
} | |
} | |
/** | |
* @description | |
* Stops the current recording. | |
* If the toast is been displayed. | |
*/ | |
function handleMediaStop() { | |
if (recorder.state !== 'inactive') { | |
recorder.stop() | |
displayToast && | |
ToastsStore.info('¡Muy bien, has grabado un audio puedes continuar o volver a intentarlo!', TOAST_EXPIRATION) | |
recording && setRecording(false) | |
} | |
audioNodeRef.current.pause() | |
} | |
/** | |
* @function | |
* Replays the current media. | |
*/ | |
function handleMediaPlay() { | |
audioNodeRef.current.play() | |
} | |
/** | |
* @helper | |
* @description Returns the icon className for giving two options. | |
* @returns {boolean} | |
*/ | |
function getIconClass(className = '') { | |
return !recording | |
? `icon-container-disabled ${className}` | |
: `icon-container-enabled ${className}` | |
} | |
return (( | |
<React.Fragment> | |
<FlexContainer justifyContent="center"> | |
{error ? ( | |
<React.Fragment> | |
Debe habilitar el micrófono en su navegador | |
</React.Fragment> | |
) : ( | |
<Button | |
variant="muted" | |
disabled={recording || disabled} | |
size="sm" | |
onClick={!recording ? handleMediaPlayback : null} | |
> | |
<Emoji name="Micro" onClick={handleMediaStop} /> | |
</Button> | |
)} | |
</FlexContainer> | |
<FlexContainer justifyContent="center"> | |
<Stopwatch | |
maxDuration={maxDuration} | |
onTimeExceed={handleMediaStop} | |
running={recording} | |
/> | |
</FlexContainer> | |
{controls && ( | |
<FlexContainer justifyContent="center"> | |
<Icon | |
className={getIconClass('hovered')} | |
icon={ic_play_arrow} | |
onClick={!recording ? handleMediaPlay : null} | |
size={28} | |
/> | |
<Icon | |
className={getIconClass('hovered')} | |
icon={ic_pause} | |
onClick={recording ? handleMediaStop : null} | |
size={28} | |
/> | |
</FlexContainer> | |
)} | |
<audio controls hidden ref={audioNodeRef} /> | |
</React.Fragment> | |
)) | |
} | |
AudioRecorder.defaultProps = { | |
controls: true, | |
onAudioStart: () => null, | |
onAudioCapture: () => null, | |
displayToast: false, | |
maxDuration: 3000, | |
format: 'audio/mpeg' | |
} | |
export default memo(AudioRecorder) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment