将MediaRecorder的数据块发送到服务器并在后端构建文件

19

我正在使用Node.js和SailsJs制作网站。

我的目标是将由MediaRecorder.ondataavailable事件生成的Blob(返回小的Blob)发送到服务器,并在录音完成后在服务器上构建完整文件以进行存储。

在浏览器上,如果我将所有这些小Blob推入数组中,然后执行以下操作:

var blob = new Blob(recordedBlobs, {type: 'video/mp4'});

我获得了完整的文件blob,可以轻松上传到服务器并且可以完全播放。

我使用ajax将所有这些小的blobs发送到服务器上,在服务器端,我使用以下代码来保存这些小的blobs:

req.file('recordingPart').upload(async function...)

我会创建一个文件并将其存储在临时文件夹中,直到我想要组装最终文件(我还发送每个部分的索引以了解后续的确切顺序)。

当用户录制结束时,我会从前端发送另一个请求,让我知道何时开始组装文件。

然后,我使用fs.readFile将临时文件的内容读取到数组中(根据索引维护顺序),如下所示:

    const body = [];
    for (let i = 0; i < recParts.length; i++){
      body[recParts[i].part.index - 1] = await readFile(recParts[i].part.tmpPath, null);
    }

然后我使用以下方式创建文件:

const videoBuffer = Buffer.from(body);
    fs.writeFile(__dirname + '/../../.tmp/recording.mp4', videoBuffer, function(err) {
      if (err) console.log(err);
      console.log('File created');
    });

一个文件已经被创建,但无法播放!

我添加了一个console.log(body),并得到以下结果:

BODY [ <Buffer 1a>,
  <Buffer 45 df a3 a3 42 86 81 01 42 f7 81 01 42 f2 81 04 42 f3 81 08 42 82 88 6d 61 74 72 6f 73 6b 61 42 87 81 04 42 85 81 02 18 53 80 67 01 ff ff ff ff ff ff ... >,
  <Buffer 46 24 82 00 20 00 00 00 00 01 21 e0 02 00 10 5c c2 62 44 f1 0d 55 69 04 a4 d1 b0 51 fc 4e 7c 5c 11 b5 f5 24 21 88 e5 26 68 d8 9b 10 3f c8 4b 15 3f 37 ... >,
  <Buffer 41 69 81 00 78 80 fb 83 7b 73 3e e7 41 8a 76 af 1f 22 60 92 f6 ac 22 40 eb ce fc 4f 43 5c 0c 45 73 e4 91 19 21 12 54 31 46 5d 0f bb a3 ba 27 cd 3d 5a ... >,
  <Buffer 41 7c 81 00 b3 80 fb 83 96 6f 9c eb f6 d4 d5 e1 49 65 66 6d 89 fd 17 f8 7d 7f fb a2 b1 a4 39 87 be 6f 24 d0 a6 b4 fa e7 74 1b 4e eb 40 8a dc a8 dc b6 ... >,
  <Buffer 41 12 82 00 b3 00 00 00 00 01 21 e0 08 00 40 15 c1 be e1 1d 56 a0 79 dc 5e a1 ca 50 dd 66 bc 34 21 8b 96 9c 90 b6 4e 51 48 f9 f5 0e 65 ec be 5e a2 8b ... >,
  <Buffer 44 3a 82 00 f0 00 00 00 00 01 21 e0 0c 00 60 27 28 d9 d7 a4 1b 6d 34 dc ca 9f c3 1e 08 7f 5d 16 a3 b9 7b 0f e5 1d 42 fb 94 4b 1e 29 93 91 57 15 cc 4a ... >,
  <Buffer 41 59 81 01 2c 80 fb 83 71 70 9a 9e 95 bc 32 37 da b1 95 1b 62 09 1e e3 98 31 81 65 a7 f0 2d 9f dc f7 c5 3c cc 46 40 a6 5b 8c 00 91 0a d2 65 ee cb cd ... >,
  <Buffer 45 22 82 01 2c 00 00 00 00 01 21 e0 10 00 80 5f d0 9e 92 ff ff 55 69 04 ed 7a 6d 5c ca c7 f8 21 f7 69 37 58 88 ae 65 d0 c9 bf ff d1 48 6d e8 4b 3a f0 ... >,
  <Buffer 41 54 81 01 68 80 fb 83 6f 6e 51 d9 a1 72 06 04 83 57 97 1f e4 10 00 ca 0e 87 d2 f9 ac 3c e9 c5 5f b9 1c 8d 32 ea 75 e5 0f 06 e0 55 1e 4d 40 8a af 63 ... >

欢迎提出任何建议


你解决了吗? - Footniko
抱歉耽搁了,我最近事情比较多,忘记发布答案了。 - jonystorm
谢谢你的回答。实际上我已经解决了我的问题(虽然和你发布的有点不同)。 - Footniko
@Footniko,你能分享一下你使用的解决方案吗?谢谢。 - user6233283
1
@JamieCorkhill,根据您使用的块格式不同,您可以在node.js中使用类似以下代码:const fileStream = fs.createWriteStream(filePath, { flags: 'a' });... fileStream.write(Buffer.from(new Uint8Array(chunkMessage))); - Footniko
显示剩余6条评论
5个回答

13

对于那些仍然对使用MediaRecorder API和WebSockets持续保存媒体流的流程感兴趣的人...

客户端:

const ws = new WebSocket(someWsUrl);
const mediaStream = new MediaStream();
const videoTrack = someStream.getVideoTracks()[0];
const audioTrack = someStream.getAudioTracks()[0];
mediaStream.addTrack(videoTrack);
mediaStream.addTrack(audioTrack);
const recorderOptions = {
  mimeType: 'video/webm',
  videoBitsPerSecond: 200000 // 0.2 Mbit/sec.
};
const mediaRecorder = new MediaRecorder(mediaStream, recorderOptions);
mediaRecorder.start(1000); // 1000 - the number of milliseconds to record into each Blob
mediaRecorder.ondataavailable = (event) => {
  console.debug('Got blob data:', event.data);
  if (event.data && event.data.size > 0) {
    ws.send(event.data);
  }
};

服务器端:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', (ws, req) => {
  const fileStream = fs.createWriteStream(filePath, { flags: 'a' });
  ws.on('message', message => {
    // Only raw blob data can be sent
    fileStream.write(Buffer.from(new Uint8Array(message)));
  });
});

只有第一块可以使用这种方法进行播放。你能帮忙吗? - ishan shah
1
我找到了解决方案!我之前同时从呼叫方和被呼叫方开始录音,这导致了一些冲突。现在我加入了一个条件,只有发起方才会开始录音。现在它正常工作了。 - ishan shah

1

另一个使用Base64编码的简单解决方案。该代码考虑了使用getUserMedia()自己的麦克风。

客户端JavaScript: (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.addEventListener('load', async (e) => {
    const constraints = {audio: true, video: false}
    const video = document.querySelector('video')
    const stream = await navigator.mediaDevices.getUserMedia(constraints)
    video.srcObject = stream
    const options = {mimeType: 'audio/webm; codecs=opus'}
    const mediaRecorder = new MediaRecorder(stream, options)

    mediaRecorder.addEventListener('dataavailable', (e) => {
        const reader = new FileReader() 
        reader.readAsDataURL(e.data)        // Using Base64 encoding
        reader.addEventListener('loadend', async (e) => {
            const chunk = reader.result.split(',').pop()
            const response = await fetch('save-audio.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    chunk: chunk
                })
            })
            const json = await response.json()
            console.log(json)
        })
    })
    document.querySelector("#start").addEventListener('click', async (e) => {
        document.querySelector("#start").disabled = true
        document.querySelector("#stop").disabled = false    
        mediaRecorder.start(500)
    })
    document.querySelector("#stop").addEventListener('click', async (e) => {
        document.querySelector("#stop").disabled = true
        mediaRecorder.stop()
        stream.getTracks().forEach(track => track.stop())
    })
})
</script>
</head>
<body>
    <button id="start">Start</button>
    <button id="stop" disabled>Stop</button>
    <video id="local-video" autoplay playsinline muted></video>
</body>
</html>

服务器端 PHP:(save_audio.php)

<?php

$request = json_decode(file_get_contents('php://input'), false);
$decoded_chunk = base64_decode($request->chunk);

$path = "file.webm";
$bytes = file_put_contents($path, $decoded_chunk, FILE_APPEND | LOCK_EX);
echo json_encode(array("status"=>"ok", "Bytes"=>$bytes));
die();

?>

1

另一种解决方案是不需要编码和 Node.js 作为后端服务器,而是使用 fetch API 替代 WebSockets。它发送块的原始数据。在追加所有块之后,使用 ffmpeg 重新创建音频文件以获取音频文件的总时长(ffmpeg 已经在操作系统中预先安装)。

客户端 JavaScript: (./public/index.html)

<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.addEventListener('load', async (e) => {
    const constraints = {audio: true, video: false} 
    const mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
    
    /******** If you like to see every single audio sample ***********
    const audioTrack = mediaStream.getAudioTracks()[0]
    const trackProcessor = new MediaStreamTrackProcessor({ track: audioTrack })
    const readableStream = trackProcessor.readable
    const readableStreamDefaultReader = readableStream.getReader()
    readableStreamDefaultReader.read().then(function processChunk( {value, done} ) {
        const audioData = value
        if (done) {
            console.log("Stream complete")
            return
        }
        else {
            console.log(audioData)
            return readableStreamDefaultReader.read().then(processChunk);
        }
    })
    */

    const video = document.querySelector('video')
    video.srcObject = mediaStream

    const options = {mimeType: 'audio/webm; codecs=opus'}
    const mediaRecorder = new MediaRecorder(mediaStream, options)
    
    mediaRecorder.addEventListener('dataavailable', async (e) => {      
        const blob = e.data
        const response = await fetch('/raw/save_chunk', {
            method: 'POST',             
            body: blob
        })
        const json = await response.json()
        console.log(json)
    })
    mediaRecorder.addEventListener('stop', async (e) => {
        const response = await fetch('/json/finish_record', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                status: 'fine'
            })
        })
        const json = await response.json()
        console.log(json)
    })
    document.querySelector("#start").addEventListener('click', async (e) => {
        document.querySelector("#start").disabled = true
        document.querySelector("#stop").disabled = false    
        mediaRecorder.start(500)
    })
    document.querySelector("#stop").addEventListener('click', async (e) => {
        document.querySelector("#start").disabled = false
        document.querySelector("#stop").disabled = true
        mediaRecorder.stop()
        mediaStream.getTracks().forEach(track => track.stop())
    })
})
</script>
</head>
<body>
    <button id="start">Start</button>
    <button id="stop" disabled>Stop</button>
    <video id="local-video" autoplay playsinline muted></video>
</body>
</html>

服务器端 NodeJs: (app.mjs)

// ES6 Static Imports =========================================================
import fs from 'fs'
import {URL} from 'url'
import express from 'express'
import https from 'https'
import {Blob} from 'buffer'
import {exec} from 'child_process'

// Constants ===========================================================
const port = 3000
const __filename = new URL('', import.meta.url).pathname
const __dirname = new URL('.', import.meta.url).pathname

// Express Server and Routing ==========================================
const app = express()

const options = {
    key:  fs.readFileSync('key.pem',  'utf8'),
    cert: fs.readFileSync('cert.pem', 'utf8'),
}

const server = https.createServer(options, app)
server.listen(port, () => {
    console.log('listening https ' + port)
})


app.use('/', express.static(__dirname + '/public'))
app.post('/raw/:cmd', express.raw({type: "*/*"}), async (req, res) => {
    const buffer = req.body
    const blob = new Blob([buffer], {type: "audio/webm;codecs=opus"})
    await fs.promises.appendFile('./server/audio.webm.tmp', buffer)
    const response = {status:200, msg:"Succesful", data: 'chunk' }  
    res.json(response)
})
app.post('/json/:cmd', express.json(), async (req, res) => {
    const params = req.body 
    const child = exec('ffmpeg -i ./server/audio.webm.tmp ./server/audio.webm', (error, stdout, stderr) => {})
    child.on('close', async () => {
        await fs.promises.unlink('./server/audio.webm.tmp')
    })  
    const response = {status:200, msg:"Succesful", data: 'finish' } 
    return(response)
})

1
所以我通过以下方式解决了它(一旦我收到合并操作调用):
const dir = `${__dirname}/.tmp/`;
const fileName = getFileNameFromEvent(eventId);
const path = dir + fileName;
//First get the path for every file chunk ordered (otherwise it'll lose quality)
let recParts = await RecordingParts.find({
      where: {
        filename: fileName
      }
    }).sort('index ASC');

let wstream = fs.createWriteStream(path);
for (let i = 0; i < recParts.length; i++){
      let aux = await readFile(recParts[i].tmpPath, null);
      wstream.write(aux);
      //Delete chunks
      fs.unlink(recParts[i].tmpPath, (err) => {
        if (err) throw err;
      });
    }

    wstream.end();

//Utils function
const readFile = (path, opts = 'utf8') =>
  new Promise((res, rej) => {
    fs.readFile(path, opts, (err, data) => {
      if (err) rej(err);
      else res(data)
    })
  });

之后

wstream.end();

您将在路径处获得合并后的文件。

1
这是我的解决方案,如果有帮助的话:
我以二进制格式发送数据块(我选择了Uint8Array),并在将接收到的数据打包后将每个数据块添加到服务器端的文件中(以相同的无符号字符解码方式转换为二进制)。 客户端JavaScript:
let order=0;
mediaRecorder.ondataavailable = async (e) => {
    if(e.data && e.data.size > 0) {         
        var reader = new FileReader();
        reader.readAsArrayBuffer(e.data); 
        reader.onloadend = async function(event) {
            let arrayBuffer = reader.result;   
            let uint8View = new Uint8Array(arrayBuffer);
            let response = await fetch('save-video.php', {
                method: 'POST',                 
                body: JSON.stringify({
                    chunk: uint8View,
                    order: order
                })                  
            });
            order += 1;
        }

    }       
}

服务器端 PHP:
<?php
    
$request = json_decode(file_get_contents("php://input"), true);
$chunk = $request['chunk'];
$order = $request['order'];
$binarydata = pack("C*", ...$chunk);
    
$filePath = "uploads/file.webm";
$out = fopen("{$filePath}", $order == 0 ? "wb" : "ab");
if ($out) {
    fwrite($out, $binarydata);
    fclose($out);
}

?>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接