0

0

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

心靈之曲

心靈之曲

发布时间:2025-12-14 19:44:15

|

844人浏览过

|

来源于php中文网

原创

使用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 修改:

天工大模型
天工大模型

中国首个对标ChatGPT的双千亿级大语言模型

下载
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')) {

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

337

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

224

2025.10.31

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

138

2026.02.12

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

562

2023.09.20

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

74

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

38

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

83

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

97

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

223

2026.03.05

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 13.4万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.3万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 1.0万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号