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

标签 javascript node.js stream electron mediarecorder-api

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

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

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

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

我有可能在不同的流对象之间有很多线交叉,但我很惊讶我还没有在网上找到解决方案 - 因此我正在弹出我的 StackOverflow 问题樱桃。

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

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

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

1. BlobreadStream

我无法转换 Blob进入 readStream , 仅 ReadableStream , ReadableStreamDefaultReaderUint8Array
2. Blobfile (在内存中)然后使用 fs.createReadStream()

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

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

我无法转换 Blobbuffer可用于 fs.createReadStream(buffer) ,只有 arrayBuffer或一个与 null字节

任何帮助是极大的赞赏!

项目:

Node 12.13.0、Chrome 80.0.3987.158 和 Electron 8.2.0。

设置:

  • main.js、package.json、index.html、record.js 这四个文件都是项目文件夹中的单层文件。

  • 每个文件的内容:

    包.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"
      }
    }
    

    主.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)
    

    索引.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>
    

    记录.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();


    运行以下命令:
    npm install -D electron
    npm start
    

    最佳答案

    好吧,我破解了……
    最终,挑战的关键在于:

    如何转换blob进入 readablestream在 node.js .

    无论如何,总而言之,我发现工作的步骤是:blob > arrayBuffer > array > buffer > readStream
    我需要以下函数将缓冲区转换为流。 ReferenceNode.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);
    
    }
    

    我终于可以管道并可以继续在数据和保存之间使用其他流函数,例如加密。 :)

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

    关于javascript - 如何使用流将 MediaRecorder Web API 输出保存到磁盘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60902935/

    相关文章:

    node.js - Mongoose 查询并按日期过滤

    android - 使用 NPR StreamProxy 实现时出现 MediaPlayer 错误

    javascript - 如何添加多个选择选项

    javascript - 查找与搜索匹配的歌曲(艺术家和/或轨道名称)

    javascript - 从 JavaScript 数组中删除元素

    javascript - 如何通过 Angular 单选按钮启用或禁用提交按钮

    javascript - Node.js automapper-ts 包不映射嵌套对象/属性

    javascript - 使用 Puppeteer 和 Node 从 DOM 中选择元素

    iOS - 同时发送和接收数据

    c# - 由于事先不知道长度,因此如何录制到Stream中?