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' ;
4
1
import { useEffect , useRef , useState , useCallback } from 'react' ;
5
2
import Button from 'react-bootstrap/Button' ;
6
3
import {
@@ -17,6 +14,7 @@ import {
17
14
FaVolumeDown ,
18
15
FaVolumeUp ,
19
16
FaRegTrashAlt ,
17
+ FaDownload
20
18
} from 'react-icons/fa' ;
21
19
import { useDispatch , useSelector } from 'react-redux' ;
22
20
import ListGroup from 'react-bootstrap/ListGroup' ;
@@ -91,7 +89,7 @@ function AudioViewer({ src }) {
91
89
height : '1.05em' ,
92
90
cursor : 'pointer' ,
93
91
color : 'red' ,
94
- paddingLeft : '2px' ,
92
+ paddingLeft : '2px'
95
93
} }
96
94
onClick = { toggleVolume }
97
95
/>
@@ -114,7 +112,7 @@ function AudioViewer({ src }) {
114
112
width : '1.23em' ,
115
113
height : '1.23em' ,
116
114
cursor : 'pointer' ,
117
- paddingLeft : '3px' ,
115
+ paddingLeft : '3px'
118
116
} }
119
117
onClick = { toggleVolume }
120
118
/>
@@ -133,7 +131,7 @@ function AudioViewer({ src }) {
133
131
cursorWidth : 3 ,
134
132
height : 200 ,
135
133
barGap : 3 ,
136
- dragToSeek : true ,
134
+ dragToSeek : true
137
135
// plugins:[
138
136
// WaveSurferRegions.create({maxLength: 60}),
139
137
// WaveSurferTimeLinePlugin.create({container: containerT.current})
@@ -163,7 +161,7 @@ function AudioViewer({ src }) {
163
161
flexDirection : 'column' ,
164
162
justifyContent : 'center' ,
165
163
alignItems : 'center' ,
166
- margin : '0 1rem 0 1rem' ,
164
+ margin : '0 1rem 0 1rem'
167
165
} }
168
166
>
169
167
< div
@@ -175,7 +173,7 @@ function AudioViewer({ src }) {
175
173
style = { {
176
174
display : 'flex' ,
177
175
justifyContent : 'center' ,
178
- alignItems : 'center' ,
176
+ alignItems : 'center'
179
177
} }
180
178
>
181
179
< Button
@@ -187,7 +185,7 @@ function AudioViewer({ src }) {
187
185
width : '40px' ,
188
186
height : '40px' ,
189
187
borderRadius : '50%' ,
190
- padding : '0' ,
188
+ padding : '0'
191
189
} }
192
190
onClick = { playPause }
193
191
>
@@ -210,14 +208,26 @@ function AudioViewer({ src }) {
210
208
}
211
209
212
210
export default function Recorder ( { submit, accompaniment } ) {
213
- // const Mp3Recorder = new MicRecorder({ bitRate: 128 }); // 128 is default already
214
211
const [ isRecording , setIsRecording ] = useState ( false ) ;
215
212
const [ blobURL , setBlobURL ] = useState ( '' ) ;
216
213
const [ blobData , setBlobData ] = useState ( ) ;
217
214
const [ blobInfo , setBlobInfo ] = useState ( [ ] ) ;
218
215
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 ( [ ] ) ;
220
219
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
+ } ;
221
231
const [ min , setMinute ] = useState ( 0 ) ;
222
232
const [ sec , setSecond ] = useState ( 0 ) ;
223
233
@@ -226,56 +236,59 @@ export default function Recorder({ submit, accompaniment }) {
226
236
const router = useRouter ( ) ;
227
237
const { slug, piece, actCategory, partType } = router . query ;
228
238
229
- useEffect ( ( ) => {
230
- setBlobInfo ( [ ] ) ;
231
- setBlobURL ( '' ) ;
232
- setBlobData ( ) ;
233
- } , [ partType ] ) ;
239
+ useEffect (
240
+ ( ) => {
241
+ setBlobInfo ( [ ] ) ;
242
+ setBlobURL ( '' ) ;
243
+ setBlobData ( ) ;
244
+ } ,
245
+ [ partType ]
246
+ ) ;
234
247
235
- const startRecording = ( ev ) => {
248
+ const startRecording = ( ) => {
236
249
if ( isBlocked ) {
237
250
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 ;
246
252
}
253
+
254
+ accompanimentRef . current . play ( ) ;
255
+ chunksRef . current = [ ] ;
256
+ mediaRecorder . start ( ) ;
257
+ setIsRecording ( true ) ;
247
258
} ;
248
259
249
- const stopRecording = ( ev ) => {
260
+ const stopRecording = ( ) => {
250
261
accompanimentRef . current . pause ( ) ;
251
262
accompanimentRef . current . load ( ) ;
263
+ mediaRecorder . stop ( ) ;
264
+ } ;
252
265
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 ) ;
270
283
} ;
271
284
272
285
const submitRecording = ( i , submissionId ) => {
273
286
const formData = new FormData ( ) ; // TODO: make filename reflect assignment
274
287
formData . append (
275
288
'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
+ } )
279
292
) ;
280
293
// dispatch(submit({ audio: formData }));
281
294
submit ( { audio : formData , submissionId } ) ;
@@ -287,50 +300,94 @@ export default function Recorder({ submit, accompaniment }) {
287
300
setBlobInfo ( newInfo ) ;
288
301
}
289
302
290
- // check for recording permissions
303
+ // Initialize MediaRecorder
291
304
useEffect ( ( ) => {
292
305
if (
293
306
typeof window !== 'undefined' &&
294
- navigator &&
295
- navigator . mediaDevices . getUserMedia
307
+ navigator ?. mediaDevices ?. getUserMedia
296
308
) {
297
309
navigator . mediaDevices
298
310
. 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
+ }
300
319
} )
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 ) ;
302
356
setIsBlocked ( false ) ;
303
357
} )
304
- . catch ( ( ) => {
358
+ . catch ( err => {
305
359
console . log ( 'Permission Denied' ) ;
306
360
setIsBlocked ( true ) ;
307
361
} ) ;
308
362
}
309
363
} , [ ] ) ;
310
364
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
+ ) ;
334
391
335
392
return (
336
393
< >
@@ -374,14 +431,21 @@ export default function Recorder({ submit, accompaniment }) {
374
431
/> */ }
375
432
< AudioViewer src = { take . url } />
376
433
< 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 >
385
449
</ div >
386
450
< div className = "minWidth" >
387
451
< StatusIndicator statusId = { `recording-take-${ i } ` } />
0 commit comments