MultiModal: addition of live recording...

This commit is contained in:
vinodkiran
2023-12-15 13:21:10 +05:30
parent c609c63f44
commit 826de70c6c
3 changed files with 118 additions and 631 deletions
+85 -494
View File
@@ -26,16 +26,7 @@ import {
Typography Typography
} from '@mui/material' } from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
import { import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot } from '@tabler/icons'
IconDownload,
IconSend,
IconUpload,
IconMicrophone,
IconPhotoPlus,
IconPlayerStop,
IconPlayerRecord,
IconCircleDot
} from '@tabler/icons'
// project import // project import
import { CodeBlock } from 'ui-component/markdown/CodeBlock' import { CodeBlock } from 'ui-component/markdown/CodeBlock'
@@ -59,6 +50,7 @@ import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png' import userPNG from 'assets/images/account.png'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper'
import DeleteIcon from '@mui/icons-material/Delete' import DeleteIcon from '@mui/icons-material/Delete'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
export const ChatMessage = ({ open, chatflowid, isDialog }) => { export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const theme = useTheme() const theme = useTheme()
@@ -84,11 +76,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
// drag & drop and file input
const fileUploadRef = useRef(null) const fileUploadRef = useRef(null)
const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads)
const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false)
const [previews, setPreviews] = useState([]) const [previews, setPreviews] = useState([])
const [isDragOver, setIsDragOver] = useState(false) const [isDragOver, setIsDragOver] = useState(false)
// recording
const [isRecording, setIsRecording] = useState(false)
const [recordingNotSupported, setRecordingNotSupported] = useState(false)
const handleDragOver = (e) => { const handleDragOver = (e) => {
if (!isChatFlowAvailableForUploads) { if (!isChatFlowAvailableForUploads) {
return return
@@ -227,6 +225,24 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
event.target.value = null event.target.value = null
} }
const addRecordingToPreviews = (blob) => {
const mimeType = blob.type.substring(0, blob.type.indexOf(';'))
// read blob and add to previews
const reader = new FileReader()
reader.readAsDataURL(blob)
reader.onloadend = () => {
const base64data = reader.result
const upload = {
data: base64data,
preview: audioUploadSVG,
type: 'audio',
name: 'audio.wav',
mime: mimeType
}
setPreviews((prevPreviews) => [...prevPreviews, upload])
}
}
const handleDragEnter = (e) => { const handleDragEnter = (e) => {
if (isChatFlowAvailableForUploads) { if (isChatFlowAvailableForUploads) {
e.preventDefault() e.preventDefault()
@@ -271,6 +287,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
setPreviews([]) setPreviews([])
} }
const onMicrophonePressed = () => {
setIsRecording(true)
startAudioRecording(setIsRecording, setRecordingNotSupported)
}
const onRecordingCancelled = () => {
cancelAudioRecording()
setIsRecording(false)
setRecordingNotSupported(false)
}
const onRecordingStopped = () => {
stopAudioRecording(addRecordingToPreviews)
setIsRecording(false)
setRecordingNotSupported(false)
}
const onSourceDialogClick = (data, title) => { const onSourceDialogClick = (data, title) => {
setSourceDialogProps({ data, title }) setSourceDialogProps({ data, title })
setSourceDialogOpen(true) setSourceDialogOpen(true)
@@ -487,8 +518,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
getIsChatflowStreamingApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid)
getAllowChatFlowUploads.request(chatflowid) getAllowChatFlowUploads.request(chatflowid)
scrollToBottom() scrollToBottom()
initAudioRecording() setIsRecording(false)
socket = socketIOClient(baseURL) socket = socketIOClient(baseURL)
socket.on('connect', () => { socket.on('connect', () => {
@@ -530,39 +560,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
onDrop={handleDrop} onDrop={handleDrop}
className={`file-drop-field`} className={`file-drop-field`}
> >
<div className={'audio-recording-container'}>
<div className='recording-control-buttons-container hide'>
<i className='cancel-recording-button' aria-hidden='true'>
<IconPlayerRecord />
</i>
<div className='recording-elapsed-time'>
<i className='red-recording-dot' aria-hidden='true'>
<IconCircleDot />
</i>
<p className='elapsed-time'></p>
</div>
<i className='stop-recording-button' aria-hidden='true'>
<IconPlayerStop />
</i>
</div>
<div className='text-indication-of-audio-playing-container'>
<p className='text-indication-of-audio-playing hide'>
Audio is playing<span>.</span>
<span>.</span>
<span>.</span>
</p>
</div>
</div>
<div className='overlay hide'>
<div className='browser-not-supporting-audio-recording-box'>
<p>To record audio, use browsers like Chrome and Firefox that support audio recording.</p>
<button type='button' className='close-browser-not-supported-box'>
Ok.
</button>
</div>
</div>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio controls className='audio-element hide'></audio>
{isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( {isDragOver && getAllowChatFlowUploads.data?.allowUploads && (
<Box className='drop-overlay'> <Box className='drop-overlay'>
<Typography variant='h2'>Drop here to upload</Typography> <Typography variant='h2'>Drop here to upload</Typography>
@@ -576,6 +573,41 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})} })}
</Box> </Box>
)} )}
{isRecording && (
<Box className='drop-overlay'>
<div className={'audio-recording-container'}>
<Typography variant='h2'>Recording</Typography>
<div className='recording-control-buttons-container'>
<i className='cancel-recording-button'>
<Button variant='outlined' color='error' onClick={onRecordingCancelled}>
Cancel
</Button>
</i>
<div className='recording-elapsed-time'>
<i className='red-recording-dot'>
<IconCircleDot />
</i>
<p id='elapsed-time'>00:00</p>
</div>
<i className='stop-recording-button'>
<Button variant='outlined' color='primary' onClick={onRecordingStopped}>
Save
</Button>
</i>
</div>
</div>
{recordingNotSupported && (
<div className='overlay hide'>
<div className='browser-not-supporting-audio-recording-box'>
<p>To record audio, use browsers like Chrome and Firefox that support audio recording.</p>
<button type='button' onClick={() => onRecordingCancelled()}>
Ok.
</button>
</div>
</div>
)}
</Box>
)}
<div className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}> <div className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}>
<div ref={ps} id='messagelist' className={'messagelist'}> <div ref={ps} id='messagelist' className={'messagelist'}>
{messages && {messages &&
@@ -804,9 +836,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
endAdornment={ endAdornment={
<> <>
{isChatFlowAvailableForUploads && ( {isChatFlowAvailableForUploads && (
<InputAdornment className={'start-recording-button'} position='end'> <InputAdornment position='end'>
<IconButton type='button' disabled={loading || !chatflowid} edge='end'> <IconButton
onClick={() => onMicrophonePressed()}
type='button'
disabled={loading || !chatflowid}
edge='end'
>
<IconMicrophone <IconMicrophone
className={'start-recording-button'}
color={ color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5' loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
} }
@@ -839,7 +877,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
</form> </form>
</div> </div>
</div> </div>
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} /> <SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
</div> </div>
) )
@@ -850,449 +887,3 @@ ChatMessage.propTypes = {
chatflowid: PropTypes.string, chatflowid: PropTypes.string,
isDialog: PropTypes.bool isDialog: PropTypes.bool
} }
// audio-recording.js ---------------
//View
let microphoneButton = document.getElementsByClassName('start-recording-button')[0]
let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0]
let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0]
let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0]
let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0]
let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0]
let overlay = document.getElementsByClassName('overlay')[0]
let audioElement = document.getElementsByClassName('audio-element')[0]
let audioElementSource = audioElement?.getElementsByTagName('source')[0]
let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0]
const initAudioRecording = () => {
microphoneButton = document.getElementsByClassName('start-recording-button')[0]
recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0]
stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0]
cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0]
elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0]
closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0]
overlay = document.getElementsByClassName('overlay')[0]
audioElement = document.getElementsByClassName('audio-element')[0]
audioElementSource = audioElement?.getElementsByTagName('source')[0]
textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0]
//Listeners
//Listen to start recording button
if (microphoneButton) microphoneButton.onclick = startAudioRecording
//Listen to stop recording button
if (stopRecordingButton) stopRecordingButton.onclick = stopAudioRecording
//Listen to cancel recording button
if (cancelRecordingButton) cancelRecordingButton.onclick = cancelAudioRecording
//Listen to when the ok button is clicked in the browser not supporting audio recording box
if (closeBrowserNotSupportedBoxButton) closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay
//Listen to when the audio being played ends
if (audioElement) audioElement.onended = hideTextIndicatorOfAudioPlaying
}
/** Displays recording control buttons */
function handleDisplayingRecordingControlButtons() {
//Hide the microphone button that starts audio recording
microphoneButton.style.display = 'none'
//Display the recording control buttons
recordingControlButtonsContainer.classList.remove('hide')
//Handle the displaying of the elapsed recording time
handleElapsedRecordingTime()
}
/** Hide the displayed recording control buttons */
function handleHidingRecordingControlButtons() {
//Display the microphone button that starts audio recording
microphoneButton.style.display = 'block'
//Hide the recording control buttons
recordingControlButtonsContainer.classList.add('hide')
//stop interval that handles both time elapsed and the red dot
clearInterval(elapsedTimeTimer)
}
/** Displays browser not supported info box for the user*/
function displayBrowserNotSupportedOverlay() {
overlay.classList.remove('hide')
}
/** Displays browser not supported info box for the user*/
function hideBrowserNotSupportedOverlay() {
overlay.classList.add('hide')
}
/** Creates a source element for the audio element in the HTML document*/
function createSourceForAudioElement() {
let sourceElement = document.createElement('source')
audioElement.appendChild(sourceElement)
audioElementSource = sourceElement
}
/** Display the text indicator of the audio being playing in the background */
function displayTextIndicatorOfAudioPlaying() {
textIndicatorOfAudiPlaying.classList.remove('hide')
}
/** Hide the text indicator of the audio being playing in the background */
function hideTextIndicatorOfAudioPlaying() {
textIndicatorOfAudiPlaying.classList.add('hide')
}
//Controller
/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/
let audioRecordStartTime
/** Stores the maximum recording time in hours to stop recording once maximum recording hour has been reached */
let maximumRecordingTimeInHours = 1
/** Stores the reference of the setInterval function that controls the timer in audio recording*/
let elapsedTimeTimer
/** Starts the audio recording*/
function startAudioRecording() {
console.log('Recording Audio...')
//If a previous audio recording is playing, pause it
let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not
console.log('paused?', !recorderAudioIsPlaying)
if (recorderAudioIsPlaying) {
audioElement.pause()
//also hide the audio playing indicator displayed on the screen
hideTextIndicatorOfAudioPlaying()
}
//start recording using the audio recording API
audioRecorder
.start()
.then(() => {
//on success
//store the recording start time to display the elapsed time according to it
audioRecordStartTime = new Date()
//display control buttons to offer the functionality of stop and cancel
handleDisplayingRecordingControlButtons()
})
.catch((error) => {
//on error
//No Browser Support Error
if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) {
console.log('To record audio, use browsers like Chrome and Firefox.')
displayBrowserNotSupportedOverlay()
}
//Error handling structure
switch (error.name) {
case 'AbortError': //error from navigator.mediaDevices.getUserMedia
console.log('An AbortError has occurred.')
break
case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia
console.log('A NotAllowedError has occurred. User might have denied permission.')
break
case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia
console.log('A NotFoundError has occurred.')
break
case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia
console.log('A NotReadableError has occurred.')
break
case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start
console.log('A SecurityError has occurred.')
break
case 'TypeError': //error from navigator.mediaDevices.getUserMedia
console.log('A TypeError has occurred.')
break
case 'InvalidStateError': //error from the MediaRecorder.start
console.log('An InvalidStateError has occurred.')
break
case 'UnknownError': //error from the MediaRecorder.start
console.log('An UnknownError has occurred.')
break
default:
console.log('An error occurred with the error name ' + error.name)
}
})
}
/** Stop the currently started audio recording & sends it
*/
function stopAudioRecording() {
console.log('Stopping Audio Recording...')
//stop the recording using the audio recording API
audioRecorder
.stop()
.then((audioAsblob) => {
//Play recorder audio
playAudio(audioAsblob)
//hide recording control button & return record icon
handleHidingRecordingControlButtons()
})
.catch((error) => {
//Error handling structure
switch (error.name) {
case 'InvalidStateError': //error from the MediaRecorder.stop
console.log('An InvalidStateError has occurred.')
break
default:
console.log('An error occurred with the error name ' + error.name)
}
})
}
/** Cancel the currently started audio recording */
function cancelAudioRecording() {
console.log('Canceling audio...')
//cancel the recording using the audio recording API
audioRecorder.cancel()
//hide recording control button & return record icon
handleHidingRecordingControlButtons()
}
/** Plays recorded audio using the audio element in the HTML document
* @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object
*/
function playAudio(recorderAudioAsBlob) {
//read content of files (Blobs) asynchronously
let reader = new FileReader()
//once content has been read
reader.onload = (e) => {
//store the base64 URL that represents the URL of the recording audio
let base64URL = e.target.result
//If this is the first audio playing, create a source element
//as pre-populating the HTML with a source of empty src causes error
if (!audioElementSource)
//if it is not defined create it (happens first time only)
createSourceForAudioElement()
//set the audio element's source using the base64 URL
audioElementSource.src = base64URL
//set the type of the audio element based on the recorded audio's Blob type
let BlobType = recorderAudioAsBlob.type.includes(';')
? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';'))
: recorderAudioAsBlob.type
audioElementSource.type = BlobType
//call the load method as it is used to update the audio element after changing the source or other settings
audioElement.load()
//play the audio after successfully setting new src and type that corresponds to the recorded audio
console.log('Playing audio...')
audioElement.play()
//Display text indicator of having the audio play in the background
displayTextIndicatorOfAudioPlaying()
}
//read content and convert it to a URL (base64)
reader.readAsDataURL(recorderAudioAsBlob)
}
/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/
function handleElapsedRecordingTime() {
//display initial time when recording begins
displayElapsedTimeDuringAudioRecording('00:00')
//create an interval that compute & displays elapsed time, as well as, animate red dot - every second
elapsedTimeTimer = setInterval(() => {
//compute the elapsed time every second
let elapsedTime = computeElapsedTime(audioRecordStartTime) //pass the actual record start time
//display the elapsed time
displayElapsedTimeDuringAudioRecording(elapsedTime)
}, 1000) //every second
}
/** Display elapsed time during audio recording
* @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss
*/
function displayElapsedTimeDuringAudioRecording(elapsedTime) {
//1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element
elapsedTimeTag.innerHTML = elapsedTime
//2. Stop the recording when the max number of hours is reached
if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) {
stopAudioRecording()
}
}
/**
* @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss
* @returns {Boolean} whether the elapsed time reached the maximum number of hours or not
*/
function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) {
//Split the elapsed time by the symbol that separates the hours, minutes and seconds :
let elapsedTimeSplit = elapsedTime.split(':')
//Turn the maximum recording time in hours to a string and pad it with zero if less than 10
let maximumRecordingTimeInHoursAsString =
maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString()
//if the elapsed time reach hours and also reach the maximum recording time in hours return true
if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true
//otherwise, return false
else return false
}
/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss
* @param {String} startTime - start time to compute the elapsed time since
* @returns {String} elapsed time in mm:ss format or hh:mm:ss format, if elapsed hours are 0.
*/
function computeElapsedTime(startTime) {
//record end time
let endTime = new Date()
//time difference in ms
let timeDiff = endTime - startTime
//convert time difference from ms to seconds
timeDiff = timeDiff / 1000
//extract integer seconds that don't form a minute using %
let seconds = Math.floor(timeDiff % 60) //ignoring incomplete seconds (floor)
//pad seconds with a zero if necessary
seconds = seconds < 10 ? '0' + seconds : seconds
//convert time difference from seconds to minutes using %
timeDiff = Math.floor(timeDiff / 60)
//extract integer minutes that don't form an hour using %
let minutes = timeDiff % 60 //no need to floor possible incomplete minutes, because they've been handled as seconds
minutes = minutes < 10 ? '0' + minutes : minutes
//convert time difference from minutes to hours
timeDiff = Math.floor(timeDiff / 60)
//extract integer hours that don't form a day using %
let hours = timeDiff % 24 //no need to floor possible incomplete hours, because they've been handled as seconds
//convert time difference from hours to days
timeDiff = Math.floor(timeDiff / 24)
// the rest of timeDiff is number of days
let days = timeDiff //add days to hours
let totalHours = hours + days * 24
totalHours = totalHours < 10 ? '0' + totalHours : totalHours
if (totalHours === '00') {
return minutes + ':' + seconds
} else {
return totalHours + ':' + minutes + ':' + seconds
}
}
//API to handle audio recording
const audioRecorder = {
/** Stores the recorded audio as Blob objects of audio data as the recording continues*/
audioBlobs: [] /*of type Blob[]*/,
/** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/
mediaRecorder: null /*of type MediaRecorder*/,
/** Stores the reference to the stream currently capturing the audio*/
streamBeingCaptured: null /*of type MediaStream*/,
/** Start recording the audio
* @returns {Promise} - returns a promise that resolves if audio recording successfully started
*/
start: function () {
//Feature Detection
if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
//Feature is not supported in browser
//return a custom error
return Promise.reject(new Error('mediaDevices API or getUserMedia method is not supported in this browser.'))
} else {
//Feature is supported in browser
//create an audio stream
return (
navigator.mediaDevices
.getUserMedia({ audio: true } /*of type MediaStreamConstraints*/)
//returns a promise that resolves to the audio stream
.then((stream) /*of type MediaStream*/ => {
//save the reference of the stream to be able to stop it when necessary
audioRecorder.streamBeingCaptured = stream
//create a media recorder instance by passing that stream into the MediaRecorder constructor
audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording
API provides functionality to easily record media*/
//clear previously saved audio Blobs, if any
audioRecorder.audioBlobs = []
//add a dataavailable event listener in order to store the audio data Blobs when recording
audioRecorder.mediaRecorder.addEventListener('dataavailable', (event) => {
//store audio Blob object
audioRecorder.audioBlobs.push(event.data)
})
//start the recording by calling the start method on the media recorder
audioRecorder.mediaRecorder.start()
})
)
/* errors are not handled in the API because if its handled and the promise is chained, the .then after the catch will be executed*/
}
},
/** Stop the started audio recording
* @returns {Promise} - returns a promise that resolves to the audio as a blob file
*/
stop: function () {
//return a promise that would return the blob or URL of the recording
return new Promise((resolve) => {
//save audio type to pass to set the Blob type
let mimeType = audioRecorder.mediaRecorder.mimeType
//listen to the stop event in order to create & return a single Blob object
audioRecorder.mediaRecorder.addEventListener('stop', () => {
//create a single blob object, as we might have gathered a few Blob objects that needs to be joined as one
let audioBlob = new Blob(audioRecorder.audioBlobs, { type: mimeType })
//resolve promise with the single audio blob representing the recorded audio
resolve(audioBlob)
})
audioRecorder.cancel()
})
},
/** Cancel audio recording*/
cancel: function () {
//stop the recording feature
audioRecorder.mediaRecorder.stop()
//stop all the tracks on the active stream in order to stop the stream
audioRecorder.stopStream()
//reset API properties for next recording
audioRecorder.resetRecordingProperties()
},
/** Stop all the tracks on the active stream in order to stop the stream and remove
* the red flashing dot showing in the tab
*/
stopStream: function () {
//stopping the capturing request by stopping all the tracks on the active stream
audioRecorder.streamBeingCaptured
.getTracks() //get all tracks from the stream
.forEach((track) /*of type MediaStreamTrack*/ => track.stop()) //stop each one
},
/** Reset all the recording properties including the media recorder and stream being captured*/
resetRecordingProperties: function () {
audioRecorder.mediaRecorder = null
audioRecorder.streamBeingCaptured = null
/*No need to remove event listeners attached to mediaRecorder as
If a DOM element which is removed is reference-free (no references pointing to it), the element itself is picked
up by the garbage collector as well as any event handlers/listeners associated with it.
getEventListeners(audioRecorder.mediaRecorder) will return an empty array of events.*/
}
}
@@ -20,6 +20,7 @@
justify-content: center; justify-content: center;
/*horizontal centering*/ /*horizontal centering*/
align-items: center; align-items: center;
background-color: white;
} }
.start-recording-button { .start-recording-button {
font-size: 70px; font-size: 70px;
@@ -40,6 +41,7 @@
align-items: center; align-items: center;
width: 334px; width: 334px;
margin-bottom: 30px; margin-bottom: 30px;
background-color: white;
} }
.cancel-recording-button, .cancel-recording-button,
.stop-recording-button { .stop-recording-button {
@@ -61,6 +63,7 @@
color: #27a527; color: #27a527;
} }
.recording-elapsed-time { .recording-elapsed-time {
font-size: 32px;
/*targeting Chrome & Safari*/ /*targeting Chrome & Safari*/
display: -webkit-flex; display: -webkit-flex;
/*targeting IE10*/ /*targeting IE10*/
@@ -1,41 +1,21 @@
/**
* @fileoverview This file contains the API to handle audio recording.
* Originally from 'https://ralzohairi.medium.com/audio-recording-in-javascript-96eed45b75ee'
*/
// audio-recording.js --------------- // audio-recording.js ---------------
//View let microphoneButton, elapsedTimeTag
let microphoneButton = document.getElementsByClassName('start-recording-button')[0]
let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0]
let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0]
let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0]
let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0]
let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0]
let overlay = document.getElementsByClassName('overlay')[0]
let audioElement = document.getElementsByClassName('audio-element')[0]
let audioElementSource = document.getElementsByClassName('audio-element')[0].getElementsByTagName('source')[0]
let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0]
//Listeners /** Initialize controls */
function initializeControls() {
//Listen to start recording button microphoneButton = document.getElementsByClassName('start-recording-button')[0]
microphoneButton.onclick = startAudioRecording }
//Listen to stop recording button
stopRecordingButton.onclick = stopAudioRecording
//Listen to cancel recording button
cancelRecordingButton.onclick = cancelAudioRecording
//Listen to when the ok button is clicked in the browser not supporting audio recording box
closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay
//Listen to when the audio being played ends
audioElement.onended = hideTextIndicatorOfAudioPlaying
/** Displays recording control buttons */ /** Displays recording control buttons */
function handleDisplayingRecordingControlButtons() { function handleDisplayingRecordingControlButtons() {
//Hide the microphone button that starts audio recording //Hide the microphone button that starts audio recording
microphoneButton.style.display = 'none' microphoneButton.style.display = 'none'
//Display the recording control buttons
recordingControlButtonsContainer.classList.remove('hide')
//Handle the displaying of the elapsed recording time //Handle the displaying of the elapsed recording time
handleElapsedRecordingTime() handleElapsedRecordingTime()
} }
@@ -45,43 +25,10 @@ function handleHidingRecordingControlButtons() {
//Display the microphone button that starts audio recording //Display the microphone button that starts audio recording
microphoneButton.style.display = 'block' microphoneButton.style.display = 'block'
//Hide the recording control buttons
recordingControlButtonsContainer.classList.add('hide')
//stop interval that handles both time elapsed and the red dot //stop interval that handles both time elapsed and the red dot
clearInterval(elapsedTimeTimer) clearInterval(elapsedTimeTimer)
} }
/** Displays browser not supported info box for the user*/
function displayBrowserNotSupportedOverlay() {
overlay.classList.remove('hide')
}
/** Displays browser not supported info box for the user*/
function hideBrowserNotSupportedOverlay() {
overlay.classList.add('hide')
}
/** Creates a source element for the audio element in the HTML document*/
function createSourceForAudioElement() {
let sourceElement = document.createElement('source')
audioElement.appendChild(sourceElement)
audioElementSource = sourceElement
}
/** Display the text indicator of the audio being playing in the background */
function displayTextIndicatorOfAudioPlaying() {
textIndicatorOfAudiPlaying.classList.remove('hide')
}
/** Hide the text indicator of the audio being playing in the background */
function hideTextIndicatorOfAudioPlaying() {
textIndicatorOfAudiPlaying.classList.add('hide')
}
//Controller
/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/ /** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/
let audioRecordStartTime let audioRecordStartTime
@@ -92,24 +39,17 @@ let maximumRecordingTimeInHours = 1
let elapsedTimeTimer let elapsedTimeTimer
/** Starts the audio recording*/ /** Starts the audio recording*/
function startAudioRecording() { export function startAudioRecording(onRecordingStart, onUnsupportedBrowser) {
console.log('Recording Audio...') initializeControls()
//If a previous audio recording is playing, pause it
let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not
console.log('paused?', !recorderAudioIsPlaying)
if (recorderAudioIsPlaying) {
audioElement.pause()
//also hide the audio playing indicator displayed on the screen
hideTextIndicatorOfAudioPlaying()
}
//start recording using the audio recording API //start recording using the audio recording API
audioRecorder audioRecorder
.start() .start()
.then(() => { .then(() => {
//on success //on success show the controls to stop and cancel the recording
if (onRecordingStart) {
onRecordingStart(true)
}
//store the recording start time to display the elapsed time according to it //store the recording start time to display the elapsed time according to it
audioRecordStartTime = new Date() audioRecordStartTime = new Date()
@@ -120,8 +60,9 @@ function startAudioRecording() {
//on error //on error
//No Browser Support Error //No Browser Support Error
if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) { if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) {
console.log('To record audio, use browsers like Chrome and Firefox.') if (onUnsupportedBrowser) {
displayBrowserNotSupportedOverlay() onUnsupportedBrowser(true)
}
} }
//Error handling structure //Error handling structure
@@ -157,18 +98,16 @@ function startAudioRecording() {
} }
/** Stop the currently started audio recording & sends it /** Stop the currently started audio recording & sends it
*/ */
function stopAudioRecording() { export function stopAudioRecording(addRecordingToPreviews) {
console.log('Stopping Audio Recording...')
//stop the recording using the audio recording API //stop the recording using the audio recording API
audioRecorder audioRecorder
.stop() .stop()
.then((audioAsblob) => { .then((audioBlob) => {
//Play recorder audio
playAudio(audioAsblob)
//hide recording control button & return record icon //hide recording control button & return record icon
handleHidingRecordingControlButtons() handleHidingRecordingControlButtons()
if (addRecordingToPreviews) {
addRecordingToPreviews(audioBlob)
}
}) })
.catch((error) => { .catch((error) => {
//Error handling structure //Error handling structure
@@ -183,9 +122,7 @@ function stopAudioRecording() {
} }
/** Cancel the currently started audio recording */ /** Cancel the currently started audio recording */
function cancelAudioRecording() { export function cancelAudioRecording() {
console.log('Canceling audio...')
//cancel the recording using the audio recording API //cancel the recording using the audio recording API
audioRecorder.cancel() audioRecorder.cancel()
@@ -193,50 +130,9 @@ function cancelAudioRecording() {
handleHidingRecordingControlButtons() handleHidingRecordingControlButtons()
} }
/** Plays recorded audio using the audio element in the HTML document
* @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object
*/
function playAudio(recorderAudioAsBlob) {
//read content of files (Blobs) asynchronously
let reader = new FileReader()
//once content has been read
reader.onload = (e) => {
//store the base64 URL that represents the URL of the recording audio
let base64URL = e.target.result
//If this is the first audio playing, create a source element
//as pre-populating the HTML with a source of empty src causes error
if (!audioElementSource)
//if it is not defined create it (happens first time only)
createSourceForAudioElement()
//set the audio element's source using the base64 URL
audioElementSource.src = base64URL
//set the type of the audio element based on the recorded audio's Blob type
let BlobType = recorderAudioAsBlob.type.includes(';')
? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';'))
: recorderAudioAsBlob.type
audioElementSource.type = BlobType
//call the load method as it is used to update the audio element after changing the source or other settings
audioElement.load()
//play the audio after successfully setting new src and type that corresponds to the recorded audio
console.log('Playing audio...')
audioElement.play()
//Display text indicator of having the audio play in the background
displayTextIndicatorOfAudioPlaying()
}
//read content and convert it to a URL (base64)
reader.readAsDataURL(recorderAudioAsBlob)
}
/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/ /** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/
function handleElapsedRecordingTime() { function handleElapsedRecordingTime() {
elapsedTimeTag = document.getElementById('elapsed-time')
//display initial time when recording begins //display initial time when recording begins
displayElapsedTimeDuringAudioRecording('00:00') displayElapsedTimeDuringAudioRecording('00:00')
@@ -255,7 +151,6 @@ function handleElapsedRecordingTime() {
function displayElapsedTimeDuringAudioRecording(elapsedTime) { function displayElapsedTimeDuringAudioRecording(elapsedTime) {
//1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element //1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element
elapsedTimeTag.innerHTML = elapsedTime elapsedTimeTag.innerHTML = elapsedTime
//2. Stop the recording when the max number of hours is reached //2. Stop the recording when the max number of hours is reached
if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) { if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) {
stopAudioRecording() stopAudioRecording()
@@ -275,9 +170,7 @@ function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) {
maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString() maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString()
//if the elapsed time reach hours and also reach the maximum recording time in hours return true //if the elapsed time reach hours and also reach the maximum recording time in hours return true
if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true return elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString
//otherwise, return false
else return false
} }
/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss /** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss
@@ -331,7 +224,7 @@ function computeElapsedTime(startTime) {
//API to handle audio recording //API to handle audio recording
const audioRecorder = { export const audioRecorder = {
/** Stores the recorded audio as Blob objects of audio data as the recording continues*/ /** Stores the recorded audio as Blob objects of audio data as the recording continues*/
audioBlobs: [] /*of type Blob[]*/, audioBlobs: [] /*of type Blob[]*/,
/** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/ /** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/
@@ -360,8 +253,8 @@ const audioRecorder = {
audioRecorder.streamBeingCaptured = stream audioRecorder.streamBeingCaptured = stream
//create a media recorder instance by passing that stream into the MediaRecorder constructor //create a media recorder instance by passing that stream into the MediaRecorder constructor
audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording audioRecorder.mediaRecorder = new MediaRecorder(stream)
API provides functionality to easily record media*/ /*the MediaRecorder interface of the MediaStream Recording API provides functionality to easily record media*/
//clear previously saved audio Blobs, if any //clear previously saved audio Blobs, if any
audioRecorder.audioBlobs = [] audioRecorder.audioBlobs = []