使用MediaRecorder录制实时音频并解决文件损坏问题

1次阅读

使用MediaRecorder录制实时音频并解决文件损坏问题

本文详细阐述了如何使用javaScript的MediaRecorder API进行实时音频录制,并通过php将其保存到服务器。核心内容包括解决录制文件损坏的关键问题,即在MediaRecorder实例化时正确指定音频MIME类型和编码器,以及处理数据块的两种策略:客户端累积发送最终Blob或服务器端追加(并强调其局限性),旨在帮助开发者生成可播放的音频文件。

引言:实时音频录制与挑战

在现代Web应用中,通过麦克风进行实时音频录制已成为常见需求。javascript的MediaRecorder API提供了强大的能力来实现这一目标。然而,在将录制的数据分块发送到服务器并保存时,开发者常会遇到一个棘手的问题:生成的音频文件无法播放或显示为损坏。这通常是由于对MediaRecorder的工作机制和音频文件格式的误解造成的。

本文将深入探讨这一问题,并提供一个基于JavaScript和PHP的解决方案,确保录制的音频文件能够正确保存和播放。

问题分析:为什么录制文件会损坏?

当使用MediaRecorder进行分块录制并将数据发送到服务器时,文件损坏的主要原因可以归结为以下两点:

  1. MIME类型和编码器定义不当: 许多开发者错误地尝试在Blob构造函数中指定音频的MIME类型和编码器(例如{ ‘type’ : ‘audio/ogg; codecs=opus’ })。然而,MediaRecorder生成的数据块的格式和编码方式,必须在其初始化时就确定。如果在MediaRecorder实例化时没有明确指定或指定了不兼容的类型,它可能会使用默认值,导致后续Blob构造中的类型声明与实际数据不符。
  2. 数据块处理不当: MediaRecorder通过ondataavailable事件分发的数据 (e.data) 是音频流的片段。对于一个连续可播放的音频文件,这些片段需要按照正确的顺序进行拼接。如果服务器端简单地使用file_put_contents覆盖文件,或者即使是追加,但没有正确处理容器格式(如OGG或WebM)的结构,最终文件仍会损坏。

解决方案一:正确初始化MediaRecorder

解决文件损坏问题的首要步骤是确保MediaRecorder在开始录制时就知道它应该生成什么格式的音频数据。这意味着需要在MediaRecorder的构造函数中明确指定mimeType。

核心修改:MediaRecorder构造函数

// ... navigator.mediaDevices.getUserMedia({ audio: true })     .then(function(stream) {         // 定义MediaRecorder选项,指定MIME类型和编码器         const mrOptions = { mimeType: 'audio/ogg; codecs=opus' };          // 在此处初始化MediaRecorder时指定选项         mediaRecorder = new MediaRecorder(stream, mrOptions);         mediaRecorder.start(2000); // 每2秒触发一次ondataavailable事件          mediaRecorder.ondataavailable = function(e) {             // 确保e.data是非空的             if (e.data.size > 0) {                 chunks.push(e.data);                 // 创建Blob时,使用MediaRecorder实际使用的MIME类型                 const blob = new Blob(chunks, { type: mediaRecorder.mimeType });                 chunks = []; // 清空chunks,准备接收下一个片段                  var reader = new FileReader();                 reader.readAsDataURL(blob);                  reader.onloadend = function() {                     var data = reader.result.split(";base64,")[1];                      requestp2("a.php", "data=" + encodeURIComponent(data));                 }             }         };     })     .catch(function(err) {         console.log('The following getUserMedia error occurred: ' + err);     }); // ...

解释:

  • mrOptions = { mimeType: ‘audio/ogg; codecs=opus’ }:这里我们明确告诉MediaRecorder,我们希望它生成OGG容器格式的音频,并使用Opus编码器。Opus是一种高效的音频编码器,非常适合语音和音乐
  • mediaRecorder = new MediaRecorder(stream, mrOptions):将定义好的选项传递给MediaRecorder构造函数。
  • const blob = new Blob(chunks, { type: mediaRecorder.mimeType });:在ondataavailable回调中创建Blob时,使用mediaRecorder.mimeType来确保Blob的类型与MediaRecorder生成的数据类型一致。

解决方案二:正确处理数据块(客户端累积 vs. 服务器端追加)

即使MediaRecorder生成了正确格式的音频数据块,如果这些块没有被正确地拼接起来,最终文件仍然会损坏。

策略一:客户端累积所有数据块,一次性发送 (推荐)

对于生成一个完整的、连续的音频文件,最健壮的方法是在客户端累积所有的e.data块,直到录制停止,然后将所有块合并成一个大的Blob,一次性发送到服务器。

客户端 JavaScript 修改:

使用MediaRecorder录制实时音频并解决文件损坏问题

短影AI

长视频一键生成精彩短视频

使用MediaRecorder录制实时音频并解决文件损坏问题 170

查看详情 使用MediaRecorder录制实时音频并解决文件损坏问题

var mediaRecorder = null; let chunks = []; // 用于累积所有数据块  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {    console.log('getUserMedia supported.');    navigator.mediaDevices.getUserMedia({ audio: true })       .then(function(stream) {         const mrOptions = { mimeType: 'audio/ogg; codecs=opus' };         mediaRecorder = new MediaRecorder(stream, mrOptions);          mediaRecorder.ondataavailable = function(e) {             if (e.data.size > 0) {                 chunks.push(e.data); // 累积数据块             }         };          // 录制停止时处理所有累积的块         mediaRecorder.onstop = function() {             const completeBlob = new Blob(chunks, { type: mediaRecorder.mimeType });             chunks = []; // 清空以便下次录制              var reader = new FileReader();             reader.readAsDataURL(completeBlob);              reader.onloadend = function() {                 var data = reader.result.split(";base64,")[1];                  // 发送完整的音频数据                 requestp2("a.php", "data=" + encodeURIComponent(data));             };         };          mediaRecorder.start(); // 开始录制,不再分段发送         // 示例:5秒后停止录制并发送数据         setTimeout(() => {             mediaRecorder.stop();          }, 5000);        })       .catch(function(err) {          console.log('The following getUserMedia error occurred: ' + err);       }); } else {    console.log('getUserMedia not supported on your browser!'); }  function requestp2(path, data) {     var http = new XMLHttpRequest();     http.open('POST', path, true);     http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');     http.send(data); }

服务器端 PHP 修改:

<?php if(isset($_POST["data"])) {     // 第一次接收到数据时创建文件,后续不再追加,因为只发送一次完整的Blob     file_put_contents("r.ogg", base64_decode($_POST["data"]));     exit;    } ?>

这种方法确保了服务器接收到的是一个完整的、格式正确的音频文件,简化了服务器端的处理逻辑。

策略二:服务器端追加数据块(需谨慎)

如果出于特定需求,必须将e.data块逐个发送到服务器,那么服务器端必须将这些块追加到文件中,而不是覆盖。

客户端 JavaScript 保持原样(但已包含mimeType修复):

// ... (MediaRecorder初始化部分,已包含mrOptions)         mediaRecorder.start(2000); // 每2秒发送一个数据块          mediaRecorder.ondataavailable = function(e) {             if (e.data.size > 0) {                 chunks.push(e.data);                 const blob = new Blob(chunks, { type: mediaRecorder.mimeType });                 chunks = [];                 var reader = new FileReader();                 reader.readAsDataURL(blob);                  reader.onloadend = function() {                     var data = reader.result.split(";base64,")[1];                      requestp2("a.php", "data=" + encodeURIComponent(data));                 }             }         } // ...

服务器端 PHP 修改:

<?php if(isset($_POST["data"])) {     // 使用 FILE_APPEND 模式追加数据     file_put_contents("r.ogg", base64_decode($_POST["data"]), FILE_APPEND);     exit;    } ?>

重要注意事项:

  • 容器格式的复杂性: 简单地追加原始音频数据块到文件,对于像OGG或WebM这样的容器格式来说,通常不足以生成一个有效的可播放文件。这些格式有复杂的头部、索引、时间戳和数据包结构。仅仅将原始数据追加在一起,可能会导致播放器无法解析文件结构。
  • 推荐: 如果必须逐块发送,服务器端需要一个专门的库来解析和重构这些音频流,以生成一个符合标准的文件。对于大多数Web应用,强烈推荐使用客户端累积并一次性发送完整Blob的策略

完整代码示例(客户端累积策略)

考虑到大多数场景下需要生成一个完整的音频文件,以下提供采用客户端累积策略的完整代码示例。

index.html (或包含JavaScript的页面)

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>Live Audio Recorder</title> </head> <body>     <h1>Live Audio Recording</h1>     <button id="startButton">Start Recording</button>     <button id="stopButton" disabled>Stop Recording</button>     <p id="status">Ready</p>      <script>         var mediaRecorder = null;         let chunks = [];         const statusElement = document.getElementById('status');         const startButton = document.getElementById('startButton');         const stopButton = document.getElementById('stopButton');          startButton.onclick = startRecording;         stopButton.onclick = stopRecording;          function startRecording() {             if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {                 statusElement.textContent = 'Requesting microphone access...';                 navigator.mediaDevices.getUserMedia({ audio: true })                     .then(function(stream) {                         statusElement.textContent = 'Microphone access granted. Recording...';                         startButton.disabled = true;                         stopButton.disabled = false;                          // 确保MediaRecorder在初始化时指定MIME类型和编码器                         const mrOptions = { mimeType: 'audio/ogg; codecs=opus' };                         mediaRecorder = new MediaRecorder(stream, mrOptions);                          mediaRecorder.ondataavailable = function(e) {                             if (e.data.size > 0) {                                 chunks.push(e.data); // 累积数据块                             }                         };                          mediaRecorder.onstop = function() {                             statusElement.textContent = 'Recording stopped. Preparing data...';                             const completeBlob = new Blob(chunks, { type: mediaRecorder.mimeType });                             chunks = []; // 清空以便下次录制                              var reader = new FileReader();                             reader.readAsDataURL(completeBlob);                              reader.onloadend = function() {                                 var data = reader.result.split(";base64,")[1];                                  statusElement.textContent = 'Sending data to server...';                                 requestp2("a.php", "data=" + encodeURIComponent(data));                             };                             // 停止后释放麦克风流                             stream.getTracks().forEach(track => track.stop());                             startButton.disabled = false;                             stopButton.disabled = true;                         };                          mediaRecorder.onstart = function() {                             statusElement.textContent = 'Recording started...';                         };                          mediaRecorder.onerror = function(event) {                             console.error('MediaRecorder error:', event.name);                             statusElement.textContent = 'Recording error: ' + event.name;                             startButton.disabled = false;                             stopButton.disabled = true;                             // 停止后释放麦克风流                             stream.getTracks().forEach(track => track.stop());                         };                          mediaRecorder.start(); // 开始录制,不设置时间间隔,直到手动停止                     })                     .catch(function(err) {                         console.log('The following getUserMedia error occurred: ' + err);                         statusElement.textContent = 'Error: ' + err.name;                         startButton.disabled = false;                         stopButton.disabled = true;                     });             } else {                 console.log('getUserMedia not supported on your browser!');                 statusElement.textContent = 'getUserMedia not supported on your browser!';             }         }          function stopRecording() {             if (mediaRecorder && mediaRecorder.state !== 'inactive') {                 mediaRecorder.stop();             }         }          function requestp2(path, data) {             var http = new XMLHttpRequest();             http.open('POST', path, true);             http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');             http.send(data);             http.onload = function() {                 if (http.status === 200) {                     statusElement.textContent = 'Audio saved successfully!';                 } else {                     statusElement.textContent = 'Error saving audio: ' + http.status;                 }             };             http.onerror = function() {                 statusElement.textContent = 'Network error while saving audio.';             };         }     </script> </body> </html>

a.php (服务器端处理文件)

<?php // 检查是否收到POST请求,并且包含'data'字段 if (isset($_POST["data"])) {     // 解码Base64数据     $audio_data = base64_decode($_POST["data"]);      // 定义保存文件的路径和名称     // 建议使用唯一文件名,例如基于时间戳或用户ID     $filename = "recorded_audio_" . time() . ".ogg";      $filepath = __DIR__ . "/" . $filename; // 保存到当前脚本所在目录      // 将解码后的数据写入文件     // 注意:这里不再使用FILE_APPEND,因为客户端发送的是一个完整的Blob     if (file_put_contents($filepath, $audio_data) !== false) {         // 成功写入文件         http_response_code(200); // 返回成功状态码         echo "Audio saved successfully as " . $filename;     } else {         // 写入文件失败         http_response_code(500); // 返回服务器内部错误状态码         echo "Failed to save audio file.";     }     exit; // 终止脚本执行 } else {     // 如果没有收到预期的数据,返回错误     http_response_code(400); // 返回Bad Request状态码     echo "No audio data received.";     exit; } ?>

重要注意事项与最佳实践

  1. 浏览器兼容性: MediaRecorder API在现代浏览器中支持良好,但具体支持的mimeType和codecs可能有所不同。在生产环境中,建议进行兼容性测试或提供备用方案。可以使用MediaRecorder.isTypeSupported()来检查浏览器是否支持特定的MIME类型。
    if (MediaRecorder.isTypeSupported('audio/ogg; codecs=opus')) {

以上就是使用MediaRecorder录制实时音频并解决文件损坏问题的详细内容,更多请关注php中文网其它相关文章!

text=ZqhQzanResources