
在使用MediaRecorder进行实时音频录制并分块上传至服务器时,常见的错误是生成的音频文件无法播放。本文将深入探讨导致此问题的原因,即MediaRecorder的`mimeType`配置不当以及服务器端文件写入方式不正确。我们将提供一套完整的解决方案,包括客户端JavaScript的MediaRecorder初始化配置、数据处理以及服务器端PHP的正确文件追加逻辑,确保实时录制的音频能够成功保存并播放。
实时音频录制问题剖析:为什么文件会损坏?
当开发者尝试通过JavaScript的MediaRecorder API实时捕获麦克风音频,将其分块编码为Base64,并通过POST请求发送到服务器,再由服务器解码并写入文件时,经常会遇到保存的.ogg或其它格式音频文件损坏、无法播放的问题。
核心原因通常有两个:
- MediaRecorder的MIME类型配置错误: 许多开发者误以为可以在生成Blob时指定媒体类型(new Blob(chunks, { ‘type’ : ‘audio/ogg; codecs=opus’ })),但实际上,MediaRecorder在初始化时就需要明确其将输出的数据格式和编码器。如果MediaRecorder不知道它应该生成什么类型的媒体数据,它可能会使用默认设置,这与你期望的Blob类型不匹配,导致数据流不连续或格式不正确。
- 服务器端文件写入方式不当: 在实时分块上传场景中,每一块数据都是整个音频流的一部分。如果服务器端每次都使用file_put_contents()直接覆盖文件,那么最终文件中只会保留最后一块数据,导致文件不完整或损坏。正确的做法是每次接收到数据时,将其追加到现有文件中。
客户端(JavaScript)的正确实现
要解决MediaRecorder的MIME类型配置问题,我们需要在实例化MediaRecorder对象时,通过第二个参数传入一个配置对象,其中包含mimeType属性。
以下是修正后的JavaScript客户端代码示例:
var mediaRecorder = null;
let chunks = [];
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
console.log('getUserMedia supported.');
navigator.mediaDevices.getUserMedia(
{
audio: true
})
.then(function(stream) {
// 关键:在MediaRecorder构造函数中指定mimeType
const mrOptions = { mimeType: 'audio/ogg; codecs=opus' };
mediaRecorder = new MediaRecorder(stream, mrOptions);
// 每2秒触发一次ondataavailable事件,收集数据块
mediaRecorder.start(2000);
mediaRecorder.ondataavailable = function(e) {
// 确保e.data中有数据
if (e.data.size > 0) {
chunks.push(e.data);
// 使用MediaRecorder实例的mimeType来创建Blob
const blob = new Blob(chunks, { type : mediaRecorder.mimeType });
chunks = []; // 清空chunks,准备接收下一个数据块
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
// 提取Base64编码的数据
var data = reader.result.split(";base64,")[1];
// 发送数据到服务器
requestp2("a.php", "data=" + encodeURIComponent(data));
}
}
};
mediaRecorder.onstop = function() {
console.log("录制停止");
// 可以在这里处理最后剩余的chunks,如果需要
};
})
.catch(function(err) {
console.log('获取麦克风权限失败: ' + err);
}
);
} else {
console.log('当前浏览器不支持getUserMedia!');
}
/**
* 封装的POST请求函数
* @param {string} path 请求路径
* @param {string} data 要发送的数据
*/
function requestp2(path, data) {
var http = new XMLHttpRequest();
http.open('POST', path, true);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function() {
if (http.readyState === 4 && http.status === 200) {
console.log("数据发送成功", http.responseText);
} else if (http.readyState === 4 && http.status !== 200) {
console.error("数据发送失败", http.status);
}
};
http.send(data);
}
代码解释:
- const mrOptions = { mimeType: ‘audio/ogg; codecs=opus’ };:这是最关键的改动。我们在这里明确告诉MediaRecorder,它应该以OGG容器格式,使用Opus编码器来生成音频数据。
- mediaRecorder = new MediaRecorder(stream, mrOptions);:将配置对象传递给MediaRecorder构造函数。
- const blob = new Blob(chunks, { type : mediaRecorder.mimeType });:在创建Blob时,我们直接使用mediaRecorder.mimeType,确保Blob的类型与MediaRecorder生成的数据类型一致。
- chunks = [];:每次ondataavailable事件触发并处理完数据后,清空chunks数组,避免数据重复或累积过多。
服务器端(PHP)的正确处理
在服务器端,PHP脚本需要接收Base64编码的数据,对其进行解码,并将其追加到文件中,而不是每次都覆盖。
以下是修正后的PHP服务器端代码示例:
<?php
// 定义保存音频文件的路径和文件名
$audioFilePath = "r.ogg";
if (isset($_POST["data"])) {
$base64Data = $_POST["data"];
// 解码Base64数据
$decodedData = base64_decode($base64Data);
if ($decodedData === false) {
error_log("Base64解码失败!");
http_response_code(400); // Bad Request
echo "Error: Base64 decoding failed.";
exit;
}
// 关键:使用FILE_APPEND标志将数据追加到文件末尾
// 如果文件不存在,file_put_contents 会尝试创建它
if (file_put_contents($audioFilePath, $decodedData, FILE_APPEND | LOCK_EX) === false) {
error_log("文件写入失败:" . $audioFilePath);
http_response_code(500); // Internal Server Error
echo "Error: Failed to write to file.";
exit;
}
echo "Success: Audio chunk saved.";
exit;
} else {
http_response_code(400); // Bad Request
echo "Error: No data received.";
exit;
}
?>
代码解释:
- file_put_contents($audioFilePath, $decodedData, FILE_APPEND | LOCK_EX):
- FILE_APPEND:这是最重要的标志,它指示file_put_contents函数将$decodedData追加到$audioFilePath文件的末尾,而不是覆盖文件内容。
- LOCK_EX:这是一个可选但推荐的标志,用于在写入文件时获取独占锁。这可以防止多个并发请求同时写入同一个文件,从而避免数据损坏或竞争条件。
关键注意事项与优化
- 文件格式与编码器选择:
-
错误处理:
- 客户端: 务必处理getUserMedia的错误(例如用户拒绝麦克风权限),以及网络请求的错误(例如服务器无响应)。
- 服务器端: 对Base64解码失败和文件写入失败进行错误日志记录和适当的HTTP响应,有助于调试和提高系统的健壮性。
-
数据块大小与频率:
- mediaRecorder.start(2000) 表示每2000毫秒(2秒)触发一次ondataavailable事件。这个间隔可以根据你的需求进行调整。较小的间隔意味着更频繁的网络请求,但实时性更好;较大的间隔则减少网络开销,但可能会增加内存中chunks的累积。
-
录制停止处理:
- 当录制停止时(例如用户点击停止按钮),mediaRecorder.stop()会被调用。这会触发一次最后的ondataavailable事件,其中包含所有剩余的未处理数据。确保在onstop事件中或在停止录制前,将这些剩余数据也发送到服务器。
-
安全性:
- 在生产环境中,不要直接将用户上传的文件名作为保存路径,应进行严格的输入验证和消毒,以防止路径遍历攻击或其他文件操作漏洞。
- 限制上传文件的大小和类型。
总结
通过正确配置MediaRecorder的mimeType并在服务器端使用FILE_APPEND模式追加数据,可以有效地解决实时分块录制音频文件损坏的问题。这个方案不仅保证了录制音频的完整性和可播放性,也为构建基于Web的实时语音应用奠定了坚实基础。在实际开发中,务必结合错误处理、安全性考量和性能优化,以提供稳定可靠的用户体验。
以上就是MediaRecorder实时音频分块录制与服务器端保存:解决文件损坏问题的详细内容,更多请关注php中文网其它相关文章!


