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
Advertisement
Answer
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.