From 826de70c6c73560b88dce0d7c070fc0947280aeb Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 15 Dec 2023 13:21:10 +0530 Subject: [PATCH] MultiModal: addition of live recording... --- .../ui/src/views/chatmessage/ChatMessage.js | 579 +++--------------- .../src/views/chatmessage/audio-recording.css | 3 + .../src/views/chatmessage/audio-recording.js | 167 +---- 3 files changed, 118 insertions(+), 631 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d7bbaf9e..7e0092cd 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -26,16 +26,7 @@ import { Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { - IconDownload, - IconSend, - IconUpload, - IconMicrophone, - IconPhotoPlus, - IconPlayerStop, - IconPlayerRecord, - IconCircleDot -} from '@tabler/icons' +import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot } from '@tabler/icons' // project import 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 { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' import DeleteIcon from '@mui/icons-material/Delete' +import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -84,11 +76,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + // drag & drop and file input const fileUploadRef = useRef(null) const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [previews, setPreviews] = useState([]) const [isDragOver, setIsDragOver] = useState(false) + + // recording + const [isRecording, setIsRecording] = useState(false) + const [recordingNotSupported, setRecordingNotSupported] = useState(false) + const handleDragOver = (e) => { if (!isChatFlowAvailableForUploads) { return @@ -227,6 +225,24 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { 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) => { if (isChatFlowAvailableForUploads) { e.preventDefault() @@ -271,6 +287,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { 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) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -487,8 +518,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { getIsChatflowStreamingApi.request(chatflowid) getAllowChatFlowUploads.request(chatflowid) scrollToBottom() - initAudioRecording() - + setIsRecording(false) socket = socketIOClient(baseURL) socket.on('connect', () => { @@ -530,39 +560,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onDrop={handleDrop} className={`file-drop-field`} > -
-
- -
- -

-
- -
-
-

- Audio is playing. - . - . -

-
-
-
-
-

To record audio, use browsers like Chrome and Firefox that support audio recording.

- -
-
- {/* eslint-disable-next-line jsx-a11y/media-has-caption */} - {isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( Drop here to upload @@ -576,6 +573,41 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })} )} + {isRecording && ( + +
+ Recording +
+ + + +
+ + + +

00:00

+
+ + + +
+
+ {recordingNotSupported && ( +
+
+

To record audio, use browsers like Chrome and Firefox that support audio recording.

+ +
+
+ )} +
+ )}
{messages && @@ -804,9 +836,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { endAdornment={ <> {isChatFlowAvailableForUploads && ( - - + + onMicrophonePressed()} + type='button' + disabled={loading || !chatflowid} + edge='end' + > {
- setSourceDialogOpen(false)} /> ) @@ -850,449 +887,3 @@ ChatMessage.propTypes = { chatflowid: PropTypes.string, 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.*/ - } -} diff --git a/packages/ui/src/views/chatmessage/audio-recording.css b/packages/ui/src/views/chatmessage/audio-recording.css index 5ba0fa50..fbca2f60 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.css +++ b/packages/ui/src/views/chatmessage/audio-recording.css @@ -20,6 +20,7 @@ justify-content: center; /*horizontal centering*/ align-items: center; + background-color: white; } .start-recording-button { font-size: 70px; @@ -40,6 +41,7 @@ align-items: center; width: 334px; margin-bottom: 30px; + background-color: white; } .cancel-recording-button, .stop-recording-button { @@ -61,6 +63,7 @@ color: #27a527; } .recording-elapsed-time { + font-size: 32px; /*targeting Chrome & Safari*/ display: -webkit-flex; /*targeting IE10*/ diff --git a/packages/ui/src/views/chatmessage/audio-recording.js b/packages/ui/src/views/chatmessage/audio-recording.js index 395443fe..f5cba001 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.js +++ b/packages/ui/src/views/chatmessage/audio-recording.js @@ -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 --------------- -//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 = document.getElementsByClassName('audio-element')[0].getElementsByTagName('source')[0] -let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] +let microphoneButton, elapsedTimeTag -//Listeners - -//Listen to start recording button -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 +/** Initialize controls */ +function initializeControls() { + microphoneButton = document.getElementsByClassName('start-recording-button')[0] +} /** 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() } @@ -45,43 +25,10 @@ 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 @@ -92,24 +39,17 @@ let maximumRecordingTimeInHours = 1 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() - } +export function startAudioRecording(onRecordingStart, onUnsupportedBrowser) { + initializeControls() //start recording using the audio recording API audioRecorder .start() .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 audioRecordStartTime = new Date() @@ -120,8 +60,9 @@ function startAudioRecording() { //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() + if (onUnsupportedBrowser) { + onUnsupportedBrowser(true) + } } //Error handling structure @@ -157,18 +98,16 @@ function startAudioRecording() { } /** Stop the currently started audio recording & sends it */ -function stopAudioRecording() { - console.log('Stopping Audio Recording...') - +export function stopAudioRecording(addRecordingToPreviews) { //stop the recording using the audio recording API audioRecorder .stop() - .then((audioAsblob) => { - //Play recorder audio - playAudio(audioAsblob) - + .then((audioBlob) => { //hide recording control button & return record icon handleHidingRecordingControlButtons() + if (addRecordingToPreviews) { + addRecordingToPreviews(audioBlob) + } }) .catch((error) => { //Error handling structure @@ -183,9 +122,7 @@ function stopAudioRecording() { } /** Cancel the currently started audio recording */ -function cancelAudioRecording() { - console.log('Canceling audio...') - +export function cancelAudioRecording() { //cancel the recording using the audio recording API audioRecorder.cancel() @@ -193,50 +130,9 @@ function cancelAudioRecording() { 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() { + elapsedTimeTag = document.getElementById('elapsed-time') //display initial time when recording begins displayElapsedTimeDuringAudioRecording('00:00') @@ -255,7 +151,6 @@ function handleElapsedRecordingTime() { 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() @@ -275,9 +170,7 @@ function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) { 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 + return elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString } /** 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 -const audioRecorder = { +export 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*/ @@ -360,8 +253,8 @@ const audioRecorder = { 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*/ + 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 = []