Skip to content

Commit d480521

Browse files
authored
Merge pull request #43 from Zamua/zamua/fix-media-recorder
Replace mic-recorder-to-mp3 with native MediaRecorder API
2 parents a4c9bfa + 0382f01 commit d480521

File tree

1 file changed

+147
-83
lines changed

1 file changed

+147
-83
lines changed

components/recorder.js

+147-83
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
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';
41
import { useEffect, useRef, useState, useCallback } from 'react';
52
import Button from 'react-bootstrap/Button';
63
import {
@@ -17,6 +14,7 @@ import {
1714
FaVolumeDown,
1815
FaVolumeUp,
1916
FaRegTrashAlt,
17+
FaDownload
2018
} from 'react-icons/fa';
2119
import { useDispatch, useSelector } from 'react-redux';
2220
import 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

212210
export 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

Comments
 (0)