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

在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>

三、关键优化说明

  1. 统一媒体流获取:仅调用一次getUserMedia获取完整音视频流,避免重复权限请求。
  2. 轨道拆分逻辑:通过stream.getVideoTracks()和stream.getAudioTracks()分离出独立的音视频轨道,创建专属MediaStream交给RecordRTC录制,实现文件分离。
  3. DOM元素修正:给audio标签补充id="audio",解决了srcObject设置失败的报错。
  4. 完善录制生命周期:分别停止音视频轨道,避免互相干扰,同时新增下载逻辑方便测试结果。

这样修改后,你就能正常录制并保存独立的音视频文件了。

内容的提问来源于stack exchange,提问作者Andrew Garrison

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 18:57:55