Skip to content

Instantly share code, notes, and snippets.

@anderjs
Last active January 20, 2020 18:26
Show Gist options
  • Save anderjs/a1cd36b549a3def98a8afbd59edc42ef to your computer and use it in GitHub Desktop.
Save anderjs/a1cd36b549a3def98a8afbd59edc42ef to your computer and use it in GitHub Desktop.
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