You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MediaRecorder仅首次dataavailable事件生成有效WebM文件求助

问题描述

我正在开发一款浏览器端音频录制器,通过OpenAI Whisper将音频转写为文本。使用MediaRecorder的timeslice选项(设置为15000毫秒,每15秒生成一个文件)时,只有首次触发dataavailable事件生成的是有效的.webm文件,后续生成的均无法正常识别(虽然硬编码了.webm后缀,且blob.type显示为audio/webm)。

前端代码

window.handleSuccess = function (stream) {
    const options = {mimeType: 'audio/webm;codecs=opus'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function (e) {

        const reader = new FileReader();
        reader.readAsArrayBuffer(e.data);
        reader.onloadend = function () {
            const blob = new Blob([new Uint8Array(reader.result)], { type: 'audio/webm' });

            recordedChunks.push(blob);

            // post data to server
            const formData = new FormData();
            formData.append('audio', blob, 'filename.webm');
            formData.append('audio_mime', blob.type);
            formData.append('meeting_id', meetingId);
            fetch('/api/audio', {
                method: 'POST',
                body: formData,
                accept: 'application/json',
            }).then((response) => {
                console.log(response);
            }).catch((error) => {
                console.error(error);
            });

        };
    });

Laravel后端代码

public function uploadAudio(Request $request): JsonResponse
{
    $mime = $request->input('audio_mime');
    $extensionMap = [
        'audio/webm' => 'webm',
    ];
    $extension = $extensionMap[$mime] ?? 'webm';
    $audioName = time() . '.' . $extension;

    $request->audio->move(storage_path('audio'), $audioName);

    Audio::create([
        'audio_file' => $audioName,
        'meeting_id' => $request->get('meeting_id'),
    ]);

    return response()->json([
        'message' => 'Audio uploaded successfully',
        'audio' => $audioName,
    ], 201);
}

问题原因与解决方案

核心问题

MediaRecorder的timeslice机制下,只有第一个dataavailable事件返回的Blob是完整的webm文件(包含文件头、元数据),后续触发的事件返回的都是webm媒体片段——没有文件头,只是增量的音频数据。直接保存这些片段的话,播放器或转写工具无法识别为完整的音频文件,即便后缀是.webm也没用。

另外你的前端代码里做了多余的FileReader转换:e.data本身就是符合要求的Blob对象,不需要转成ArrayBuffer再重新创建Blob,这一步完全没必要,虽然不影响核心问题,但可以简化。

解决方案

根据你的需求(用Whisper转写),有两种可行方案:

方案1:后端拼接片段后再转写

  • 前端:去掉多余的FileReader转换,直接上传e.data:
    mediaRecorder.addEventListener('dataavailable', function (e) {
        // 直接使用e.data,无需转换
        const blob = e.data;
        recordedChunks.push(blob);
    
        const formData = new FormData();
        formData.append('audio', blob, `chunk-${Date.now()}.webm`); // 给每个chunk加唯一标识
        formData.append('audio_mime', blob.type);
        formData.append('meeting_id', meetingId);
        // 后续fetch逻辑不变
    });
    
  • 后端:每个会议的音频chunk单独存储,等录制结束后,将同一个meeting_id下的所有chunk按顺序拼接成完整的webm文件,再传给Whisper转写。

方案2:让每个chunk成为可独立处理的音频

如果你需要每个15秒的chunk都能单独转写,有两种方式:

  1. 直接传递opus编码数据给Whisper:因为你用的是audio/webm;codecs=opus,每个chunk里的核心是opus帧。可以前端将Blob转成ArrayBuffer后提取opus数据,或者后端直接读取文件的二进制内容,Whisper支持直接处理opus格式的音频。
  2. 给每个chunk添加webm文件头:这种方式比较复杂,需要手动构造webm的文件头和元数据,将后续的媒体片段包装成完整文件。你可以参考webm的格式规范来实现。

额外提示

  • 测试时可以用ffmpeg检查上传的文件:ffmpeg -i your-file.webm,如果是片段的话会报错,完整文件则会显示音频信息。
  • 确保MediaRecorder的start方法正确传入了timeslice参数:mediaRecorder.start(15000);(你的代码里没贴这部分,确认是否正确调用)。

内容的提问来源于stack exchange,提问作者Jimmie Johansson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 21:38:40