Here you go (posted from my second account):
hooks.Recorder = {
async mounted() {
let firstBlob = null; // contains file header information that we need to store
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: "audio/ogg; codecs=opus",
audioBitsPerSecond: 128000,
});
// Create an audio context and connect the stream source to an analyzer node
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
const analyzer = audioContext.createAnalyser();
source.connect(analyzer);
const array = new Uint8Array(analyzer.fftSize);
function getPeakLevel() {
analyzer.getByteTimeDomainData(array);
return (
array.reduce(
(max, current) => Math.max(max, Math.abs(current - 127)),
0
) / 128
);
}
const reader = new FileReader();
reader.onloadend = () => {
const base64String = reader.result.split(",")[1];
const payload = {
chunk: base64String,
};
this.pushEvent("send_chunk", payload);
};
this.mediaRecorder.ondataavailable = (e) => {
let data = null;
if (firstBlob && e.data.size < 25000) {
return;
}
if (!firstBlob) {
firstBlob = e.data;
}
data = new Blob([firstBlob, e.data], { type: e.type }); // prepend the file information which is stored in the first blob
reader.readAsDataURL(data);
};
let lastTick = 0;
let now = 0;
let peakLevels = [];
const samplingFreq = 80;
const sampingWindow = 3;
const tick = () => {
if (!this.mediaRecorder || this.mediaRecorder.state !== "recording") {
return;
}
now = performance.now();
if (now - lastTick > samplingFreq || !firstBlob) {
const peakLevel = getPeakLevel();
peakLevels = [peakLevel, ...peakLevels];
if (peakLevels.length > sampingWindow) {
peakLevels.pop();
}
if (
(peakLevels.length === sampingWindow &&
!peakLevels.find((level) => level > 0.01)) ||
!firstBlob
) {
// all levels have been less than 0.01 so we have a longer silence and can trigger subs
this.mediaRecorder.requestData();
peakLevels = [];
}
lastTick = performance.now();
}
setTimeout(() => {
tick();
}, samplingFreq);
};
const startBtn = this.el.querySelector("#start");
startBtn.addEventListener("click", () => {
this.mediaRecorder.start();
now = performance.now();
lastTick = performance.now();
// This is a hack to generate the first chunk which can't be empty
setTimeout(() => {
tick();
}, 300);
});
const stopBtn = this.el.querySelector("#stop");
stopBtn.addEventListener("click", () => {
this.mediaRecorder.stop();
});
},
};
As I said, disclaimer, this was just hacked together for a PoC.