1- // with thanks to https://medium.com/front-end-weekly/recording-audio-in-mp3-using-reactjs-under-5-minutes-5e960defaf10
2-
3- import MicRecorder from 'mic-recorder-to-mp3' ;
41import { useEffect , useRef , useState , useCallback } from 'react' ;
52import Button from 'react-bootstrap/Button' ;
63import {
@@ -17,6 +14,7 @@ import {
1714 FaVolumeDown ,
1815 FaVolumeUp ,
1916 FaRegTrashAlt ,
17+ FaDownload
2018} from 'react-icons/fa' ;
2119import { useDispatch , useSelector } from 'react-redux' ;
2220import ListGroup from 'react-bootstrap/ListGroup' ;
@@ -91,7 +89,7 @@ function AudioViewer({ src }) {
9189 height : '1.05em' ,
9290 cursor : 'pointer' ,
9391 color : 'red' ,
94- paddingLeft : '2px' ,
92+ paddingLeft : '2px'
9593 } }
9694 onClick = { toggleVolume }
9795 />
@@ -114,7 +112,7 @@ function AudioViewer({ src }) {
114112 width : '1.23em' ,
115113 height : '1.23em' ,
116114 cursor : 'pointer' ,
117- paddingLeft : '3px' ,
115+ paddingLeft : '3px'
118116 } }
119117 onClick = { toggleVolume }
120118 />
@@ -133,7 +131,7 @@ function AudioViewer({ src }) {
133131 cursorWidth : 3 ,
134132 height : 200 ,
135133 barGap : 3 ,
136- dragToSeek : true ,
134+ dragToSeek : true
137135 // plugins:[
138136 // WaveSurferRegions.create({maxLength: 60}),
139137 // WaveSurferTimeLinePlugin.create({container: containerT.current})
@@ -163,7 +161,7 @@ function AudioViewer({ src }) {
163161 flexDirection : 'column' ,
164162 justifyContent : 'center' ,
165163 alignItems : 'center' ,
166- margin : '0 1rem 0 1rem' ,
164+ margin : '0 1rem 0 1rem'
167165 } }
168166 >
169167 < div
@@ -175,7 +173,7 @@ function AudioViewer({ src }) {
175173 style = { {
176174 display : 'flex' ,
177175 justifyContent : 'center' ,
178- alignItems : 'center' ,
176+ alignItems : 'center'
179177 } }
180178 >
181179 < Button
@@ -187,7 +185,7 @@ function AudioViewer({ src }) {
187185 width : '40px' ,
188186 height : '40px' ,
189187 borderRadius : '50%' ,
190- padding : '0' ,
188+ padding : '0'
191189 } }
192190 onClick = { playPause }
193191 >
@@ -210,14 +208,26 @@ function AudioViewer({ src }) {
210208}
211209
212210export default function Recorder ( { submit, accompaniment } ) {
213- // const Mp3Recorder = new MicRecorder({ bitRate: 128 }); // 128 is default already
214211 const [ isRecording , setIsRecording ] = useState ( false ) ;
215212 const [ blobURL , setBlobURL ] = useState ( '' ) ;
216213 const [ blobData , setBlobData ] = useState ( ) ;
217214 const [ blobInfo , setBlobInfo ] = useState ( [ ] ) ;
218215 const [ isBlocked , setIsBlocked ] = useState ( false ) ;
219- const [ recorder , setRecorder ] = useState ( new MicRecorder ( ) ) ;
216+ const [ mediaRecorder , setMediaRecorder ] = useState ( null ) ;
217+ const [ mimeType , setMimeType ] = useState ( null ) ;
218+ const chunksRef = useRef ( [ ] ) ;
220219 const dispatch = useDispatch ( ) ;
220+
221+ const getSupportedMimeType = ( ) => {
222+ const types = [
223+ 'audio/webm' ,
224+ 'audio/webm;codecs=opus' ,
225+ 'audio/ogg;codecs=opus' ,
226+ 'audio/mp4' ,
227+ 'audio/mpeg'
228+ ] ;
229+ return types . find ( type => MediaRecorder . isTypeSupported ( type ) ) || null ;
230+ } ;
221231 const [ min , setMinute ] = useState ( 0 ) ;
222232 const [ sec , setSecond ] = useState ( 0 ) ;
223233
@@ -226,56 +236,59 @@ export default function Recorder({ submit, accompaniment }) {
226236 const router = useRouter ( ) ;
227237 const { slug, piece, actCategory, partType } = router . query ;
228238
229- useEffect ( ( ) => {
230- setBlobInfo ( [ ] ) ;
231- setBlobURL ( '' ) ;
232- setBlobData ( ) ;
233- } , [ partType ] ) ;
239+ useEffect (
240+ ( ) => {
241+ setBlobInfo ( [ ] ) ;
242+ setBlobURL ( '' ) ;
243+ setBlobData ( ) ;
244+ } ,
245+ [ partType ]
246+ ) ;
234247
235- const startRecording = ( ev ) => {
248+ const startRecording = ( ) => {
236249 if ( isBlocked ) {
237250 console . error ( 'cannot record, microphone permissions are blocked' ) ;
238- } else {
239- accompanimentRef . current . play ( ) ;
240- recorder
241- . start ( )
242- . then ( ( ) => {
243- setIsRecording ( true ) ;
244- } )
245- . catch ( ( err ) => console . error ( 'problem starting recording' , err ) ) ;
251+ return ;
246252 }
253+
254+ accompanimentRef . current . play ( ) ;
255+ chunksRef . current = [ ] ;
256+ mediaRecorder . start ( ) ;
257+ setIsRecording ( true ) ;
247258 } ;
248259
249- const stopRecording = ( ev ) => {
260+ const stopRecording = ( ) => {
250261 accompanimentRef . current . pause ( ) ;
251262 accompanimentRef . current . load ( ) ;
263+ mediaRecorder . stop ( ) ;
264+ } ;
252265
253- recorder
254- . stop ( )
255- . getMp3 ( )
256- . then ( ( [ buffer , blob ] ) => {
257- setBlobData ( blob ) ;
258- const url = URL . createObjectURL ( blob ) ;
259- setBlobURL ( url ) ;
260- setBlobInfo ( [
261- ... blobInfo ,
262- {
263- url ,
264- data : blob ,
265- } ,
266- ] ) ;
267- setIsRecording ( false ) ;
268- } )
269- . catch ( ( e ) => console . error ( 'error stopping recording' , e ) ) ;
266+ const downloadRecording = i => {
267+ const url = window . URL . createObjectURL ( blobInfo [ i ] . data ) ;
268+ const a = document . createElement ( 'a' ) ;
269+ a . style . display = 'none' ;
270+ a . href = url ;
271+ const extension = mimeType . includes ( 'webm' )
272+ ? 'webm'
273+ : mimeType . includes ( 'ogg' )
274+ ? 'ogg'
275+ : mimeType . includes ( 'mp4' )
276+ ? 'm4a'
277+ : 'wav' ;
278+ a . download = `recording- ${ i + 1 } . ${ extension } ` ;
279+ document . body . appendChild ( a ) ;
280+ a . click ( ) ;
281+ window . URL . revokeObjectURL ( url ) ;
282+ document . body . removeChild ( a ) ;
270283 } ;
271284
272285 const submitRecording = ( i , submissionId ) => {
273286 const formData = new FormData ( ) ; // TODO: make filename reflect assignment
274287 formData . append (
275288 'file' ,
276- new File ( [ blobInfo [ i ] . data ] , 'student-recoding.mp3 ' , {
277- mimeType : 'audio/mpeg' ,
278- } ) ,
289+ new File ( [ blobInfo [ i ] . data ] , 'student-recording ' , {
290+ type : mimeType
291+ } )
279292 ) ;
280293 // dispatch(submit({ audio: formData }));
281294 submit ( { audio : formData , submissionId } ) ;
@@ -287,50 +300,94 @@ export default function Recorder({ submit, accompaniment }) {
287300 setBlobInfo ( newInfo ) ;
288301 }
289302
290- // check for recording permissions
303+ // Initialize MediaRecorder
291304 useEffect ( ( ) => {
292305 if (
293306 typeof window !== 'undefined' &&
294- navigator &&
295- navigator . mediaDevices . getUserMedia
307+ navigator ?. mediaDevices ?. getUserMedia
296308 ) {
297309 navigator . mediaDevices
298310 . getUserMedia ( {
299- audio : { echoCancellation : false , noiseSuppression : false } ,
311+ audio : {
312+ echoCancellation : false ,
313+ noiseSuppression : false ,
314+ autoGainControl : false ,
315+ channelCount : 1 ,
316+ sampleRate : 48000 ,
317+ latency : 0
318+ }
300319 } )
301- . then ( ( ) => {
320+ . then ( stream => {
321+ const supportedType = getSupportedMimeType ( ) ;
322+ if ( ! supportedType ) {
323+ console . error ( 'No supported audio MIME type found' ) ;
324+ setIsBlocked ( true ) ;
325+ return ;
326+ }
327+ setMimeType ( supportedType ) ;
328+
329+ const recorder = new MediaRecorder ( stream , {
330+ mimeType : supportedType
331+ } ) ;
332+
333+ recorder . ondataavailable = e => {
334+ if ( e . data . size > 0 ) {
335+ chunksRef . current . push ( e . data ) ;
336+ }
337+ } ;
338+
339+ recorder . onstop = ( ) => {
340+ const blob = new Blob ( chunksRef . current , { type : supportedType } ) ;
341+ setBlobData ( blob ) ;
342+ const url = URL . createObjectURL ( blob ) ;
343+ setBlobURL ( url ) ;
344+ setBlobInfo ( prevInfo => [
345+ ...prevInfo ,
346+ {
347+ url,
348+ data : blob
349+ }
350+ ] ) ;
351+ setIsRecording ( false ) ;
352+ chunksRef . current = [ ] ;
353+ } ;
354+
355+ setMediaRecorder ( recorder ) ;
302356 setIsBlocked ( false ) ;
303357 } )
304- . catch ( ( ) => {
358+ . catch ( err => {
305359 console . log ( 'Permission Denied' ) ;
306360 setIsBlocked ( true ) ;
307361 } ) ;
308362 }
309363 } , [ ] ) ;
310364
311- useEffect ( ( ) => {
312- let interval = null ;
313- if ( isRecording ) {
314- interval = setInterval ( ( ) => {
315- setSecond ( sec + 1 ) ;
316- if ( sec === 59 ) {
317- setMinute ( min + 1 ) ;
318- setSecond ( 0 ) ;
319- }
320- if ( min === 99 ) {
321- setMinute ( 0 ) ;
322- setSecond ( 0 ) ;
323- }
324- } , 1000 ) ;
325- } else if ( ! isRecording && sec !== 0 ) {
326- setMinute ( 0 ) ;
327- setSecond ( 0 ) ;
328- clearInterval ( interval ) ;
329- }
330- return ( ) => {
331- clearInterval ( interval ) ;
332- } ;
333- } , [ isRecording , sec ] ) ;
365+ useEffect (
366+ ( ) => {
367+ let interval = null ;
368+ if ( isRecording ) {
369+ interval = setInterval ( ( ) => {
370+ setSecond ( sec + 1 ) ;
371+ if ( sec === 59 ) {
372+ setMinute ( min + 1 ) ;
373+ setSecond ( 0 ) ;
374+ }
375+ if ( min === 99 ) {
376+ setMinute ( 0 ) ;
377+ setSecond ( 0 ) ;
378+ }
379+ } , 1000 ) ;
380+ } else if ( ! isRecording && sec !== 0 ) {
381+ setMinute ( 0 ) ;
382+ setSecond ( 0 ) ;
383+ clearInterval ( interval ) ;
384+ }
385+ return ( ) => {
386+ clearInterval ( interval ) ;
387+ } ;
388+ } ,
389+ [ isRecording , sec ]
390+ ) ;
334391
335392 return (
336393 < >
@@ -374,14 +431,21 @@ export default function Recorder({ submit, accompaniment }) {
374431 /> */ }
375432 < AudioViewer src = { take . url } />
376433 < div >
377- < Button
378- onClick = { ( ) => submitRecording ( i , `recording-take-${ i } ` ) }
379- >
380- < FaCloudUploadAlt />
381- </ Button >
382- < Button onClick = { ( ) => deleteTake ( i ) } >
383- < FaRegTrashAlt />
384- </ Button >
434+ < div style = { { display : 'flex' , gap : '0.5rem' } } >
435+ < Button
436+ onClick = { ( ) =>
437+ submitRecording ( i , `recording-take-${ i } ` )
438+ }
439+ >
440+ < FaCloudUploadAlt />
441+ </ Button >
442+ < Button onClick = { ( ) => downloadRecording ( i ) } >
443+ < FaDownload />
444+ </ Button >
445+ < Button onClick = { ( ) => deleteTake ( i ) } >
446+ < FaRegTrashAlt />
447+ </ Button >
448+ </ div >
385449 </ div >
386450 < div className = "minWidth" >
387451 < StatusIndicator statusId = { `recording-take-${ i } ` } />
0 commit comments