'How to save MediaRecorder Web API output to disk using a stream
I am experimenting with the MediaStream Recording API within Electron (therefore Node.js) and wish to handle the output as a stream. Handling as a stream would allow me to process the MediaRecorder output before saving to disk - I could encrypt it, for example. For my specific use case I am just concerned with audio, so I do not have any video elements recording.
My most basic use case is to simply save the output to disk using a stream, but I cannot seem to achieve this fundamental task, so I will focus this question on achieving this.
Question: How to save MediaRecorder Web API output to disk using a stream.
I can save a file to disk using a download “hack”, provided and described as such by Google here, and successfully use node.js fs to open, transform (encrypt), save a new encrypted file, and delete the unencrypted file. This means that I ultimately have to save unencrypted data to disk. Even if for a short amount of time, this feels like a security compromise that I thought would be easy to avoid by encrypting before saving.
There is a risk I am getting quite a few wires crossed between different stream objects, but I am surprised I have not found a solution online yet - therefore I am popping my StackOverflow question cherry.
A project highlighting all I have tried is below. The key code in is record.js, in the save() function.
Ultimately, I am trying to create a suitable readStream
to plug into the writeStream
created with const writeStream = fs.createWriteStream(fPath);
using readStream.pipe(writeStream)
.
In summary, I have tried the following:
1. Blob
to readStream
I cannot convert Blob
into readStream
, only ReadableStream
, ReadableStreamDefaultReader
or Uint8Array
2. Blob
to file
(in memory) and then use fs.createReadStream()
I cannot seem to use an ObjectURL
in fs.createReadStream(url)
, it insists on appending a local Path.
The answer to this question suggests this is a limitation of fs.createReadStream()
and using http.get()
or request()
is not suitable in my case because I am not trying to access a remote resource.
3. Blob
to buffer
and then use fs.createReadStream()
I cannot convert Blob
to a buffer
that can be used in fs.createReadStream(buffer)
, only an arrayBuffer
or one with null
bytes
Any help is greatly appreciated!
Project:
Node 12.13.0, Chrome 80.0.3987.158, and Electron 8.2.0.
Set-up:
- the four files: main.js, package.json, index.html, record.js are all single level in the project folder.
Contents of each file:
package.json:
{
"name": "mediarecorderapi",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^8.2.0"
}
}
main.js:
const { app, BrowserWindow, ipcMain } = require('electron');
function createWindow () {
// Create the browser window.
let win = new BrowserWindow({
width: 1000,
height: 800,
x:0,
y:0,
title: "Media Recorder Example",
webPreferences: {
nodeIntegration: true,
devTools: true
}
})
win.openDevTools();
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<h1>Hello World!</h1>
We are using node <script>document.write(process.versions.node)</script>,
Chrome <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
<br/><br/>
<div>
<button id="button_rec">Record</button>
<p>recorder state: <span id="rec_status">inactive</span></p>
</div>
</body>
<script src="record.js"></script>
</html>
record.js:
console.log("hello world from record.js()");
const remote = require('electron').remote;
const path = require('path');
const fs = require('fs');
const appDir = remote.app.getPath('userData');
var recButton = document.getElementById("button_rec");
var recStatusSpan = document.getElementById("rec_status");
var recorder;
init = async function () {
// html page event handlers:
recButton.addEventListener("click", () => {record()});
// SET UP MEDIA RECORDER:
var audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
recorder = new MediaRecorder(audioStream, {mimeType: 'audio/webm'});
chunks = [];
recorder.onstart = (event) => {
// ...
}
recorder.ondataavailable = (event) => {
chunks.push(event.data);
}
recorder.onstop = async (event) => {
let fileName = `audiofile_${Date.now().toString()}.webm`;
// download(chunks, fileName); // <== This works at downloading the file to disk, but this is not a stream. Use to prove that audio is being recorded and that it can be saved.
save(chunks, fileName); // <== Trying to save using a stream
chunks = [];
}
}
record = function() {
if(recorder.state == "inactive"){
recorder.start();
recButton.innerHTML = "Stop Recording";
} else {
recorder.stop();
recButton.innerHTML = "Record";
}
recStatusSpan.innerHTML = recorder.state;
}
download = function (audioToSave, fName) {
let audioBlob = new Blob(audioToSave, {
type: "audio/webm"
});
let url = URL.createObjectURL(audioBlob);
let a = document.createElement("a");
a.style = "display: none";
a.href = url;
document.body.appendChild(a);
a.download = fName;
a.click();
// release / remove
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
save = async function (audioToSave, fName){
let fPath = path.join(appDir, fName);
console.log(`Tring to save to: ${fPath}`);
// create the writeStream - this line creates the 0kb file, ready to be written to
const writeStream = fs.createWriteStream(`${fPath}`);
console.log(writeStream); // :) WriteStream {...}
// The following lines are ultimately trying to get to a suitable readStream to pipe into the writeStream using readStream.pipe(writeStream):
// Multiple attempts written out - uncomment the method you are trying...
// The incoming data 'audioToSave' is an array containing a single blob of data.
console.log(audioToSave); // [Blob]
// ================
// METHOD 1: Stream a Blob:
// Issue: I cannot find a method to convert a Blob to a "readStream"
// ================
// Lets convert the data to a Blob
var audioBlob = new Blob(audioToSave, {
type: "audio/webm"
});
console.log(audioBlob); // Blob {size: 9876, type: "audio/webm"}
// And lets convert the Blob to a Stream
var audioBlobReadableStream = audioBlob.stream(); // https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream
console.log(audioBlobReadableStream ); // ReadableStream {locked: false}
// audioBlobReadableStream.pipe(writeStream); // ERROR: Uncaught (in promise) TypeError: audioBlobReadableStream .pipe is not a function
// audioBlobReadableStream.pipeTo(writeStream); // ERROR: TypeError: Failed to execute 'pipeTo' on 'audioBlobReadableStream': Illegal invocation
// converting the ReadableStream into a ReadableStreamDefaultReader:
var audioBlobReadableStreamDefaultReader = await audioBlobReadableStream.getReader();
console.log(audioBlobReadableStreamDefaultReader) // ReadableStreamDefaultReader {closed: Promise}
// audioBlobReadableStreamDefaultReader.pipe(writeStream); // ERROR: TypeError: audioBlobReadableStreamDefaultReader.pipe is not a function
// audioBlobReadableStreamDefaultReader.pipeTo(writeStream); // ERROR: TypeError: audioBlobReadableStreamDefaultReader.pipeTo is not a function
// And read the reader:
var audioBlobReadStream = await audioBlobReadableStreamDefaultReader.read();
console.log(audioBlobReadStream); // {value: Uint8Array(9876), done: false}
// audioBlobReadStream.pipe(writeStream); // ERROR: TypeError: audioBlobReadStream.pipe is not a function
// audioBlobReadStream.pipeTo(writeStream); // ERROR: TypeError: audioBlobReadStream.pipeTo is not a function
// ================
// METHOD 2: Blob to file, use fs
// Note, fs.createReadStream() requires a string, Buffer, or URL
// Issue: I cannot convert a Blob to a file i can access with fs without downloading it
// ================
// // Or convert to a file (to try to help fs.read)
var audioFile = new File([audioBlob], "audioFileName", { type: 'audio/webm' });
console.log(audioFile); // File {...}
// ====
// a: url
// Issue: fs.createReadStream(url) adds a local path to the objectURL created, and this local path obviously doesn't exist
// ====
var url = URL.createObjectURL(audioFile);
console.log(url); // blob:file:///{GUID}
const fileReadStream = fs.createReadStream(url); // ERROR: events.js:187 ENOENT: no such file or directory, open 'C:\... [Local Path] ...\blob:file:\19428f7d-768a-4eff-b551-4068daa8ceb6'
console.log(fileReadStream); // ReadStream {... path: "blob:file:///{GUID}" ...}
// fileReadStream.pipe(writeStream);
// ====
// b: buffer
// Issue: I cannot convert a blob to a buffer that I can insert into fs.createReadStream(buffer)
// ====
var audioArrayBuffer = await audioBlob.arrayBuffer();
console.log(audioArrayBuffer); // ArrayBuffer(9876)
// bufferReadStream = fs.createReadStream(audioArrayBuffer); // ERROR: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be one of type string, Buffer, or URL. Received type object
let audioBuffer = toBuffer(audioArrayBuffer)
console.log(audioBuffer);
let bufferReadStream = fs.createReadStream(audioBuffer); // ERROR: TypeError [ERR_INVALID_ARG_VALUE]: The argument 'path' must be a string or Uint8Array without null bytes. Received <Buffer 1a 45 ...
function toBuffer(ab) {
// FROM: https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer
var buf = Buffer.alloc(ab.byteLength);
var view = new Uint8Array(ab);
for (var i = 0; i < buf.length; ++i) {
buf[i] = view[i];
}
return buf;
}
}
init();
Run the following:
npm install -D electron
npm start
Solution 1:[1]
OK, I cracked it… Ultimately, the crux of the challenge was:
how to convert blob
into readablestream
in node.js.
Anyway, in summary the steps I found to work are: blob
> arrayBuffer
> array
> buffer
> readStream
I needed the following function to convert a buffer to a stream. Reference and Node.js docs:
let { Readable } = require('stream') ;
function bufferToStream(buffer) {
let stream = new Readable ();
stream.push(buffer);
stream.push(null);
return stream;
}
The rest of the conversion steps are one-liners and the full save function is here:
save = async function (audioToSave, fPath) {
console.log(`Trying to save to: ${fPath}`);
// create the writeStream - this line creates the 0kb file, ready to be written to
const writeStream = fs.createWriteStream(fPath);
console.log(writeStream); // WriteStream {...}
// The incoming data 'audioToSave' is an array containing a single blob of data.
console.log(audioToSave); // [Blob]
// Lets convert the data to a Blob
var audioBlob = new Blob(audioToSave, {
type: "audio/webm"
});
console.log(audioBlob); // Blob {size: 17955, type: "audio/webm"}
// note: audioBlob = audio[0] has same effect
// now we go through the following process: blob > arrayBuffer > array > buffer > readStream:
const arrayBuffer = await audioBlob.arrayBuffer();
console.log(arrayBuffer); // ArrayBuffer(17955) {}
const array = new Uint8Array(arrayBuffer);
console.log(array); // Uint8Array(17955) [26, 69, ... ]
const buffer = Buffer.from(array);
console.log(buffer); // Buffer(17955) [26, 69, ... ]
let readStream = bufferToStream(buffer);
console.log(readStream); // Readable {_readableState: ReadableState, readable: true, ... }
// and now we can pipe:
readStream.pipe(writeStream);
}
And I can finally pipe and can continue using other stream functions between the data and the save, for example, encryption. :)
Hope this helps someone else too.
Solution 2:[2]
Alternatively, you can simplify your approach a bit, since Buffer.from()
works directly on the array buffer and you can use Readable.from(buffer)
to convert a buffer into a ReadableStream.
import { Blob } from 'buffer';
import fs from 'fs';
import { Readable } from 'stream';
const writeStream = fs.createWriteStream(filePath);
// chunks is an array of blobs; you get one of those blobs
// from the `MediaRecorder.ondataavailable` event.data
const chunks = [audioblob1, audioblob2, ...];
const audio = new Blob(chunks, { type: "audio/webm" });
const buffer = Buffer.from(await audio.arrayBuffer());
const readStream = Readable.from(buffer);
readStream.pipe(writeStream).on('finish', () => {
console.log('? audio saved');
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Adam Marsh |
Solution 2 | Splines |