
本文详细阐述了如何使用JavaScript的MediaRecorder API进行实时音频录制,并通过PHP将其保存到服务器。核心内容包括解决录制文件损坏的关键问题,即在MediaRecorder实例化时正确指定音频MIME类型和编码器,以及处理数据块的两种策略:客户端累积发送最终Blob或服务器端追加(并强调其局限性),旨在帮助开发者生成可播放的音频文件。
在现代Web应用中,通过麦克风进行实时音频录制已成为常见需求。JavaScript的MediaRecorder API提供了强大的能力来实现这一目标。然而,在将录制的数据分块发送到服务器并保存时,开发者常会遇到一个棘手的问题:生成的音频文件无法播放或显示为损坏。这通常是由于对MediaRecorder的工作机制和音频文件格式的误解造成的。
本文将深入探讨这一问题,并提供一个基于JavaScript和PHP的解决方案,确保录制的音频文件能够正确保存和播放。
当使用MediaRecorder进行分块录制并将数据发送到服务器时,文件损坏的主要原因可以归结为以下两点:
解决文件损坏问题的首要步骤是确保MediaRecorder在开始录制时就知道它应该生成什么格式的音频数据。这意味着需要在MediaRecorder的构造函数中明确指定mimeType。
// ...
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);
});
// ...解释:
即使MediaRecorder生成了正确格式的音频数据块,如果这些块没有被正确地拼接起来,最终文件仍然会损坏。
对于生成一个完整的、连续的音频文件,最健壮的方法是在客户端累积所有的e.data块,直到录制停止,然后将所有块合并成一个大的Blob,一次性发送到服务器。
客户端 JavaScript 修改:
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;
}
?>重要注意事项:
考虑到大多数场景下需要生成一个完整的音频文件,以下提供采用客户端累积策略的完整代码示例。
<!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><?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;
}
?>
if (MediaRecorder.isTypeSupported('audio/ogg; codecs=opus')) {以上就是使用MediaRecorder录制实时音频并解决文件损坏问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号