
简介这份资源面向PC端前端开发者聚焦H5调用麦克风获取实时音频流与录音上传的完整实现适合需要落地语音识别、在线通话或实时音频处理场景的中级开发者参考。包内共9个文件以3个js脚本和2个html页面为核心配合1个mp3音频、1个png示例图、1个cs后台处理文件与1个ashx接口文件压缩包约189KB结构紧凑覆盖前端采集与后台接收两端。内容围绕Web Audio API与MediaRecorder展开演示如何通过getUserMedia请求麦克风权限、创建AudioContext处理音频流并将录制的分块数据合并为Blob后上传至服务器同时给出PC端浏览器兼容性检查思路。已有7407人学习下载读者可据此快速搭建可运行的录音上传Demo理解实时音频流处理链路与前后端交互方式并在此基础上扩展音量调节、音频分析等节点减少从零调试的成本。1. 浏览器里那支看不见的麦克风从 getUserMedia 到后台落盘的完整链路很多前端第一次接麦克风需求代码写完在本地localhost跑得好好的一部署到测试环境就报navigator.mediaDevices是 undefined页面白屏。这不是玄学是安全上下文在卡你——浏览器只允许 HTTPS 或 localhost 下访问麦克风HTTP 域名一律拒绝。这个标题要解决的就是一条完整链路前端怎么拿到实时音频流、怎么边播边录、怎么把录好的音频上传到后台。适合正在做语音输入、在线客服、会议记录、口语测评这类功能的前端也适合需要理解前端音频数据形态的后端。整条链路涉及三个核心对象MediaStream、MediaRecorder、Blob搞清它们的关系后面所有参数和坑都有解。2. 拿到实时音频流getUserMedia 的约束参数与设备选择2.1 为什么先要 MediaStream 而不是直接录音浏览器的音频采集是分层的。最底层是navigator.mediaDevices.getUserMedia()它返回一个MediaStream对象你可以把它理解成一条活的管道里面装着来自麦克风的实时音频轨道MediaStreamTrack。这条管道有两个用途一是直接塞给audio元素做实时监听二是交给MediaRecorder做录制。很多人一上来就找录音 API其实录音只是这条管道的一个消费者。MediaStream本身不存数据它是流式的你不消费它就浪费掉了。所以正确的顺序是先拿到流确认设备正常再决定是监听、录制还是两者都要。这个顺序搞反就会出现录音文件是空的这种血泪经验。2.2 getUserMedia 的最小可用调用与约束参数下面这段是能直接抄的最小实现重点看audio里的约束对象// 获取麦克风音频流audio 传对象表示带约束 async function getMicStream() { try { const stream await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, // 回声消除通话场景必开 noiseSuppression: true, // 降噪嘈杂环境有用 autoGainControl: true, // 自动增益避免声音忽大忽小 sampleRate: 48000, // 采样率48000 是多数设备原生值 channelCount: 1 // 单声道足够省带宽 }, video: false // 只要音频别顺手开摄像头 }); return stream; } catch (err) { // 不同 err.name 对应不同处理见 2.3 console.error(获取麦克风失败:, err.name, err.message); throw err; } }逻辑说明getUserMedia是异步的必须await。audio传true表示用默认约束传对象才能精细控制。video: false很关键只要音频时千万别开视频否则会额外弹摄像头授权用户直接懵。参数说明echoCancellation、noiseSuppression、autoGainControl这三个是布尔开关浏览器和硬件支持程度不一设了不一定生效但不设肯定没有。sampleRate是期望值实际以设备为准你写 48000 设备只支持 44100浏览器会给你 44100不会报错。channelCount: 1对语音场景足够立体声只会让上传体积翻倍。2.3 授权失败的四种 err.name 与对应处理getUserMedia抛出的错误err.name是最有价值的排查线索别只打印 messageerr.name含义处理方式NotAllowedError用户拒绝或系统策略禁止提示用户去浏览器设置里手动允许NotFoundError没有找到麦克风设备提示检查设备连接NotReadableError设备被其他程序占用提示关闭其他占用麦克风的软件OverconstrainedError约束条件无法满足降级约束比如去掉 sampleRateNotReadableError在 Windows 上特别常见用户开着会议软件再打开你的页面就会撞上。OverconstrainedError则是你自己挖的坑约束写太死设备满足不了。稳妥做法是首次用宽松约束拿到流之后再track.applyConstraints()微调。2.4 枚举设备与切换麦克风当用户有多个麦克风比如笔记本内置 外接耳机需要让用户选。注意enumerateDevices在授权前拿不到设备 label必须先调一次getUserMedia拿到授权// 先授权再枚举否则 label 是空字符串 async function listMics() { await getMicStream(); // 触发一次授权 const devices await navigator.mediaDevices.enumerateDevices(); return devices .filter(d d.kind audioinput) .map(d ({ id: d.deviceId, label: d.label || 未命名麦克风 })); } // 切换到指定设备 async function switchMic(deviceId) { return navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: deviceId } } }); }逻辑说明enumerateDevices返回所有输入输出设备用kind audioinput过滤出麦克风。deviceId用exact精确匹配切换设备时旧流记得track.stop()释放否则两个麦克风同时占用在部分系统上会互相干扰。3. 边录边传MediaRecorder 的编码选型与分片策略3.1 MediaRecorder 支持哪些格式怎么选MediaRecorder是浏览器原生录音 API它把MediaStream编码成音频数据。格式用mimeType指定但浏览器支持情况差异很大必须先探测// 按优先级探测可用格式 function pickMimeType() { const candidates [ audio/webm;codecsopus, // Chrome/Firefox 首选压缩率高 audio/webm, // 退而求其次 audio/ogg;codecsopus, // Firefox 老版本 audio/mp4 // Safari 走这条 ]; return candidates.find(t MediaRecorder.isTypeSupported(t)) || ; }逻辑说明MediaRecorder.isTypeSupported是静态方法返回布尔值。按优先级从高到低找第一个支持的。opus编码在语音场景性价比最高低码率下音质依然能打。参数说明Safari 对 webm 支持一直不好audio/mp4是它的主路径。如果你的用户有 iOS后台必须能同时解析 webm 和 mp4否则会出现安卓能传、苹果传上来是坏的这种翻车。3.2 用 timeslice 做分片实现边录边传默认情况下MediaRecorder只在stop()时吐一个完整 Blob长录音会占大量内存上传也慢。用start(timeslice)可以每隔一段时间吐一个分片function createRecorder(stream) { const mimeType pickMimeType(); const recorder new MediaRecorder(stream, { mimeType, audioBitsPerSecond: 64000 // 语音 64kbps 足够别设太高 }); const chunks []; // 每 1000ms 吐一个分片 recorder.ondataavailable (e) { if (e.data e.data.size 0) { chunks.push(e.data); // 这里可以顺手把 e.data 上传实现真正的边录边传 } }; recorder.onstop () { const blob new Blob(chunks, { type: mimeType }); // blob 就是完整录音交给上传逻辑 uploadAudio(blob); }; recorder.start(1000); return recorder; }逻辑说明start(1000)表示每 1000 毫秒触发一次ondataavailable把数据推到chunks数组。onstop时把所有分片合成一个 Blob。如果想边录边传就在ondataavailable里直接上传分片后台按顺序拼接。参数说明audioBitsPerSecond是目标码率语音 64000 足够设太高文件大且没意义。timeslice设太小比如 100ms会频繁触发回调增加开销设太大比如 10000ms就失去分片意义。1000 到 3000 是常见区间。3.3 暂停、恢复与时长统计MediaRecorder有pause()和resume()但要注意暂停期间不产生数据时长统计得自己算let startTime 0; let accumulated 0; // 已累计的录音毫秒数 recorder.onstart () { startTime Date.now(); }; recorder.onpause () { accumulated Date.now() - startTime; }; recorder.onresume () { startTime Date.now(); }; function getDuration() { const running recorder.state recording ? Date.now() - startTime : 0; return accumulated running; }逻辑说明暂停时把当前段时长累加进accumulated恢复时重置startTime。getDuration根据当前状态决定是否加上正在录的这段。这个逻辑不写暂停后再恢复时长就会算错。3.4 实时音频流怎么同时给到监听和可视化录音的同时用户往往想看到波形这就要用AudioContext分析实时流。注意AudioContext和MediaRecorder消费的是同一条流互不影响function setupVisualizer(stream) { const ctx new AudioContext(); const source ctx.createMediaStreamSource(stream); const analyser ctx.createAnalyser(); analyser.fftSize 256; // 决定频率分辨率越小越省 source.connect(analyser); const data new Uint8Array(analyser.frequencyBinCount); function draw() { analyser.getByteFrequencyData(data); // data 里就是频谱数据拿去画柱状图 requestAnimationFrame(draw); } draw(); }逻辑说明createMediaStreamSource把流接入音频图analyser做频谱分析。getByteFrequencyData把当前频谱填进data数组值域 0-255。fftSize决定frequencyBinCount等于 fftSize 的一半256 对应 128 个频点画波形够用。4. 上传到后台Blob 封装、FormData 与断点续传4.1 Blob 到 FormData 的标准上传写法录好的 Blob 不能直接当请求体发标准做法是塞进FormDataasync function uploadAudio(blob) { const form new FormData(); // 第三个参数是文件名后台靠它识别扩展名 form.append(file, blob, record_${Date.now()}.webm); form.append(duration, String(getDuration())); form.append(sampleRate, 48000); const res await fetch(/api/audio/upload, { method: POST, body: form // 千万别手动设 Content-Type浏览器会自动带 boundary }); if (!res.ok) { throw new Error(上传失败: ${res.status}); } return res.json(); }逻辑说明FormData.append第三个参数指定文件名后台很多框架靠这个扩展名判断类型不传会变成默认的blob后台可能拒绝。fetch发 FormData 时不要手动设Content-Type设了反而丢失 boundary后台解析直接失败这是新手最常见的翻车点。参数说明duration、sampleRate这些元数据一起传后台转码或做时长校验时不用再解析音频文件省事。4.2 大文件分片上传与断点续传录音超过几分钟文件可能几十 MB一次性上传容易超时。分片上传的思路是前端切片、后台合并async function uploadInChunks(blob, chunkSize 1024 * 1024) { const total Math.ceil(blob.size / chunkSize); const fileId ${Date.now()}_${Math.random().toString(36).slice(2)}; for (let i 0; i total; i) { const chunk blob.slice(i * chunkSize, (i 1) * chunkSize); const form new FormData(); form.append(file, chunk); form.append(fileId, fileId); form.append(index, String(i)); form.append(total, String(total)); await fetch(/api/audio/chunk, { method: POST, body: form }); } // 通知后台合并 await fetch(/api/audio/merge, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fileId, total }) }); }逻辑说明blob.slice按字节切分每片带上fileId、index、total后台按fileId归组、按index排序合并。断点续传就是在循环里记录已成功的index重试时跳过。参数说明chunkSize一般 1MB 到 5MB太小请求多太大失去分片意义。fileId用时间戳加随机串保证唯一。4.3 后台接收端要注意的三件事前端传上来的音频后台常见处理是存文件或转码。有三点必须注意第一multipart/form-data的解析要配好大小限制很多框架默认限制几 MB录音一超就 413第二webm 和 mp4 两种格式都要能接别只写一种第三文件名别直接用前端传的做一次白名单校验防止路径穿越。如果后台要转成统一格式比如 wav 或 mp3常见做法是用 ffmpeg 在服务端转码前端不用管。转码是异步的接口先返回已接收转码完成再回调或轮询。5. 避坑与排查麦克风链路上最容易翻车的五个点5.1 现象部署后 navigator.mediaDevices 是 undefined原因页面不是安全上下文。浏览器规定只有 HTTPS 或 localhost 才能访问麦克风HTTP 域名下navigator.mediaDevices直接不存在。解决给站点配 HTTPS 证书。本地开发用localhost或127.0.0.1不受限。内网测试如果只能用 IP可以临时用自签证书或者让后端反代一层 HTTPS。5.2 现象录音文件能播但没声音或者只有开头一小段原因MediaRecorder的ondataavailable没触发或者stop()调太早最后一片数据还没吐出来就合成 Blob 了。解决确保start()传了 timesliceonstop里再合成 Blob。如果手动调stop()要等onstop回调触发后再处理数据别在stop()后面直接读chunks。5.3 现象上传接口返回 400后台说解析不到文件原因手动设置了Content-Type: multipart/form-data但没带 boundary或者 boundary 和实际不符。解决用fetch发FormData时删掉手动设置的Content-Type让浏览器自动生成。用 axios 的话别在全局拦截器里给所有请求塞Content-Type。5.4 现象iOS Safari 上传的音频后台解析失败原因Safari 不支持 webm实际录出来是 mp4但前端mimeType写死了 webmBlob 的 type 和内容对不上。解决用MediaRecorder.isTypeSupported动态选格式Blob 的 type 用实际选中的mimeType。后台同时支持 webm 和 mp4 解析。5.5 现象切换页面或锁屏后录音中断原因浏览器对后台标签页会节流MediaRecorder在页面不可见时可能暂停或降频。解决这是浏览器行为无法完全绕过。产品上要提示用户保持页面在前台。如果必须后台录考虑用AudioWorklet自己处理音频数据但复杂度高很多一般场景不值得。6. 进阶用 AudioWorklet 拿到原始 PCM 做实时处理MediaRecorder给你的是编码后的数据如果你要做实时降噪、语音识别推流、或者自定义编码就需要原始 PCM。AudioWorklet是现在唯一推荐的方案ScriptProcessorNode已经废弃。思路是写一个 worklet 处理器在音频线程里拿到每帧的 Float32 数据转成 Int16 通过postMessage发回主线程// pcm-processor.js作为独立文件加载 class PCMProcessor extends AudioWorkletProcessor { process(inputs) { const input inputs[0]; if (input input[0]) { const float32 input[0]; // 单声道 const int16 new Int16Array(float32.length); for (let i 0; i float32.length; i) { // Float32 [-1,1] 映射到 Int16 [-32768,32767] int16[i] Math.max(-1, Math.min(1, float32[i])) * 0x7fff; } this.port.postMessage(int16.buffer, [int16.buffer]); } return true; // 返回 true 保持处理器存活 } } registerProcessor(pcm-processor, PCMProcessor);主线程这样接async function setupPCM(stream) { const ctx new AudioContext({ sampleRate: 16000 }); // 语音识别常用 16k await ctx.audioWorklet.addModule(/pcm-processor.js); const source ctx.createMediaStreamSource(stream); const worklet new AudioWorkletNode(ctx, pcm-processor); worklet.port.onmessage (e) { const pcm new Int16Array(e.data); // pcm 就是原始数据可以推给 WebSocket 做实时识别 }; source.connect(worklet); // 注意worklet 不连 destination 也能工作避免回声 }逻辑说明AudioWorkletProcessor的process方法在音频线程每 128 帧调用一次inputs[0][0]就是第一路输入的单声道 Float32 数据。转 Int16 是因为多数语音识别接口要 16 位 PCM。postMessage第二个参数是 transferable把 buffer 转移过去避免拷贝。参数说明AudioContext的sampleRate设 16000 是语音识别惯例设 48000 再重采样也行但多一步。process返回true表示持续处理返回false处理器会被回收。验证方法把收到的 PCM 存成文件用 Audacity 以16 位有符号、小端、单声道导入能正常播放就说明转换没错。这一步我踩过坑字节序搞反了听出来全是噪音。我自己的习惯是任何麦克风项目先在localhost把授权、录音、上传三段分别跑通再合起来。三段里最容易出问题的是上传那段的Content-Type我至少在这上面翻过三次车。希望帮到你。本文还有配套的精品资源点击获取