在WebRTC中多次调用getUserMedia实现音视频分离录制
问题分析与解决方案
首先,你的核心需求——分开录制音视频并保存为独立文件——是完全可以实现的,但当前代码里的几个明显错误导致了TypeError: Cannot set property 'srcObject' of null报错,我们一步步拆解问题并给出修正方案:
一、当前代码的直接错误点
- Audio标签缺失ID属性:你的HTML里的audio标签是
<audio style="display: none;"></audio>,没有添加id="audio",导致var audio = document.getElementById('audio');获取到的是null,自然无法设置srcObject,这是触发报错的直接原因。 - 变量名与参数错误:在音频录制代码块中,你写了
audiorecorder = RecordRTC(microphone, options);,但这里的microphone应该是前面定义的mic变量,而且options完全未定义,会导致RecordRTC初始化失败。 - 重复请求媒体权限:两次调用
getUserMedia属于冗余操作,浏览器会重复触发权限请求(即使用户已允许),一次获取完整音视频流再拆分轨道是更高效的做法。
二、修正后的可行实现方案
我们可以通过拆分媒体轨道的方式,从同一个完整媒体流中分离出音频和视频轨道,分别交给RecordRTC录制,既避免重复权限请求,又能实现音视频文件分离的需求。以下是修正后的完整代码:
<html> <head> <meta charset="utf-8"> </head> <body> <div class="camera"> <!-- 给audio标签添加id属性 --> <audio id="audio" style="display: none;"></audio> <video id="video" autoplay controls></video><br> <button id="startbutton">Start Recording</button> <button id="stopbutton">Stop Recording</button> <button id="photobutton">Take photo</button> </div> <canvas id="canvas" style="display: none;"></canvas> <div class="output"> <img id="photo"> </div> <button id="downloadbutton" style="display: none;">Download All</button> <script src="RecordRTC.js"></script> <script src="https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js"></script> <script src="https://www.webrtc-experiment.com/EBML.js"></script> <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script src="https://www.webrtc-experiment.com/DetectRTC.js"></script> <script> var startbutton = document.getElementById('startbutton'); var stopbutton = document.getElementById('stopbutton'); var video = document.getElementById('video'); var audio = document.getElementById('audio'); var canvas = document.getElementById('canvas'); var photo = document.getElementById('photo'); var photobutton = document.getElementById('photobutton'); var downloadbutton = document.getElementById('downloadbutton'); var videorecorder; var audiorecorder; var originalStream; // 保存原始完整音视频流 var width = 320; var height = 0; var streaming = false; stopbutton.disabled = true; downloadbutton.style.display = 'none'; function stopVideoRecordingCallback() { video.srcObject = null; video.muted = false; video.volume = 1; video.src = URL.createObjectURL(videorecorder.getBlob()); // 停止视频轨道,不影响音频流 videorecorder.stream.getTracks().forEach(track => track.stop()); videorecorder.destroy(); videorecorder = null; } function stopAudioRecordingCallback() { audio.srcObject = null; audio.src = URL.createObjectURL(audiorecorder.getBlob()); // 停止音频轨道 audiorecorder.stream.getTracks().forEach(track => track.stop()); audiorecorder.destroy(); audiorecorder = null; // 录制完成后显示下载按钮 downloadbutton.style.display = 'block'; } function clearphoto() { var context = canvas.getContext('2d'); context.fillStyle = "#AAA"; context.fillRect(0, 0, canvas.width, canvas.height); var data = canvas.toDataURL('image/png'); photo.setAttribute('src', data); } function takepicture() { var context = canvas.getContext('2d'); if (width && height) { canvas.width = width; canvas.height = height; context.drawImage(video, 0, 0, width, height); var data = canvas.toDataURL('image/png'); photo.setAttribute('src', data); } else { clearphoto(); } } startbutton.addEventListener('click', function() { // 一次获取完整音视频流 navigator.mediaDevices.getUserMedia({video: true, audio: true}).then(function(stream) { originalStream = stream; // 创建仅包含视频轨道的流,用于视频预览与录制 const videoStream = new MediaStream(stream.getVideoTracks()); video.srcObject = videoStream; videorecorder = RecordRTC(videoStream, { type: 'video', mimeType: 'video/webm' }); videorecorder.startRecording(); // 创建仅包含音频轨道的流,用于音频录制 const audioStream = new MediaStream(stream.getAudioTracks()); audio.srcObject = audioStream; audiorecorder = RecordRTC(audioStream, { type: 'audio', mimeType: 'audio/webm' }); audiorecorder.startRecording(); startbutton.disabled = true; stopbutton.disabled = false; video.addEventListener('canplay', function(ev){ if (!streaming) { height = video.videoHeight / (video.videoWidth/width); if (isNaN(height)) { height = width / (4/3); } video.setAttribute('width', width); video.setAttribute('height', height); canvas.setAttribute('width', width); canvas.setAttribute('height', height); streaming = true; } }, false); photobutton.addEventListener('click', function(ev){ takepicture(); ev.preventDefault(); }, false); clearphoto(); }).catch(function(err) { console.log("An error occurred: " + err); }); }); stopbutton.addEventListener('click', function(){ this.disabled = true; videorecorder.stopRecording(stopVideoRecordingCallback); audiorecorder.stopRecording(stopAudioRecordingCallback); }); // 添加下载按钮逻辑,方便获取录制的音视频文件 downloadbutton.addEventListener('click', function() { if (videorecorder && audiorecorder) { // 下载视频文件 const videoBlob = videorecorder.getBlob(); const videoUrl = URL.createObjectURL(videoBlob); const videoLink = document.createElement('a'); videoLink.href = videoUrl; videoLink.download = 'recording-video.webm'; videoLink.click(); // 下载音频文件 const audioBlob = audiorecorder.getBlob(); const audioUrl = URL.createObjectURL(audioBlob); const audioLink = document.createElement('a'); audioLink.href = audioUrl; audioLink.download = 'recording-audio.webm'; audioLink.click(); } }); </script> </body> </html>
三、关键优化说明
- 统一媒体流获取:仅调用一次
getUserMedia获取完整音视频流,避免重复权限请求。 - 轨道拆分逻辑:通过
stream.getVideoTracks()和stream.getAudioTracks()分离出独立的音视频轨道,创建专属MediaStream交给RecordRTC录制,实现文件分离。 - DOM元素修正:给audio标签补充
id="audio",解决了srcObject设置失败的报错。 - 完善录制生命周期:分别停止音视频轨道,避免互相干扰,同时新增下载逻辑方便测试结果。
这样修改后,你就能正常录制并保存独立的音视频文件了。
内容的提问来源于stack exchange,提问作者Andrew Garrison
相关产品推荐
相关产品推荐

