如何使用流将 MediaRecorder Web API 输出保存到磁盘

How to save MediaRecorder Web API output to disk using a stream

我正在试验 MediaStream Recording API within Electron (therefore Node.js) 并希望将输出作为流处理。作为流处理将允许我在 保存到磁盘之前处理 MediaRecorder 输出 - 例如,我可以对其进行加密。对于我的具体用例,我只关心音频,所以我没有录制任何视频元素。

我最基本的用例是简单地使用流将输出保存到磁盘,但我似乎无法完成这项基本任务,所以我将把这个问题集中在实现这一点上。

问题:如何使用流将 MediaRecorder Web API 输出保存到磁盘。

我可以使用下载“hack”将文件保存到磁盘,provided and described as such by Google here, and successfully use node.js fs 打开、转换(加密)、保存新的加密文件并删除未加密的文件。这意味着我最终必须将未加密的数据保存到磁盘。即使在很短的时间内,这感觉像是一种安全妥协,我认为通过在保存前加密可以很容易地避免这种情况。

我在不同的流对象之间有很多线交叉的风险,但令我惊讶的是我还没有在网上找到解决方案 - 因此我正在弹出我的 Whosebug 问题 cherry。

下面是一个突出显示我尝试过的所有项目的项目。关键代码在record.js,在save()函数中

最终,我正在尝试创建一个合适的 readStream 以插入使用 readStream.pipe(writeStream) 使用 const writeStream = fs.createWriteStream(fPath); 创建的 writeStream

总而言之,我尝试了以下方法:

1. BlobreadStream

我无法将 Blob 转换为 readStream,只能将 ReadableStreamReadableStreamDefaultReaderUint8Array

2。 Blobfile (在内存中)然后使用 fs.createReadStream()

我似乎无法在 fs.createReadStream(url) 中使用 ObjectURL,它坚持要附加一个本地路径。 的答案表明这是 fs.createReadStream() 的限制,使用 http.get()request() 不适合我的情况,因为我没有尝试访问远程资源。

3。 Blobbuffer 然后使用 fs.createReadStream()

我无法将 Blob 转换为可在 fs.createReadStream(buffer) 中使用的 buffer,只能转换为 arrayBuffer 或具有 null 字节的 [=46] =]

非常感谢任何帮助!


项目:

节点 12.13.0,Chrome 80.0.3987.158,和电子 8.2.0.

设置:

  • 四个文件:main.js,package.json,index.htmlrecord.js都是项目文件夹中的单层

每个文件的内容:

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:428f7d-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: 
        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();

运行 如下:

npm install -D electron
npm start

好的,我破解了…… 最终,挑战的症结在于:

如何在node.js中将blob转换为readablestream

无论如何,总而言之,我发现可行的步骤是:blob > arrayBuffer > array > buffer > readStream

我需要以下函数将缓冲区转换为流。 Reference and Node.js docs:

let { Readable } = require('stream') ;

function bufferToStream(buffer) {
    let stream = new Readable ();
    stream.push(buffer);
    stream.push(null);
    return stream;
}

剩下的转换步骤是一行,完整的保存功能在这里:

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);

}

而且我终于可以通过管道传输并可以继续在数据和保存之间使用其他流功能,例如加密。 :)

希望这对其他人也有帮助。

或者,您可以稍微简化 ,因为 Buffer.from() 直接作用于数组缓冲区,您可以使用 Readable.from(buffer) 将缓冲区转换为 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');
});