WebRTC(peerjs)通话中途新增媒体流(非替换)实现方法
PeerJS 音视频通话初始无视频轨时中途新增视频流实现方案
问题根因
你之前直接调用remotePeerConnection.addTrack()失效的核心原因有三点:
- 调用原生
addTrack时没有传入关联的MediaStream对象,对端收到新轨道后无法将其归类到已有的通话流中 - 仅调用
addTrack不会自动触发WebRTC的连接重协商流程,新添加的轨道不会被编码传输到对端 - PeerJS默认的
stream事件仅在通话首次建立时触发,后续新增轨道不会自动触发该事件,需要手动监听track事件处理新轨道
正确实现步骤
1. 调整全局变量存储,保留PeerJS call实例引用
之前的逻辑仅存储了原生RTCPeerConnection实例,需要额外存储PeerJS返回的call对象,方便后续监听轨道事件、处理协商流程:
const peer = new Peer() // 新增存储PeerJS call实例的变量 let currentCall = null let remotePeerConnection = null let audioStream; let videoStream; let mixedStream = new MediaStream() // 初始获取音频流的逻辑保持不变,初始仅音频通话时不需要提前获取视频流 try{ audioStream = await navigator.mediaDevices.getUserMedia({audio:true}) if(audioStream) mixedStream.addTrack(audioStream.getAudioTracks()[0]) } catch(err){ audioStream = null; } // 本地预览逻辑保持不变 const myVideo = document.getElementById("my-video") myVideo.srcObject = mixedStream myVideo.play()
2. 修改通话绑定逻辑,监听中途新增的轨道事件
不管是主叫发起通话还是被叫接听通话,都需要绑定track事件,用来接收通话过程中对端新增的媒体轨道:
// 主叫发起通话方法 const call = (peerID) => { currentCall = peer.call(peerID,mixedStream) remotePeerConnection = currentCall.peerConnection bindCallEvents(currentCall) } // 被叫接听逻辑 peer.on('call', (incomingCall) => { currentCall = incomingCall remotePeerConnection = currentCall.peerConnection // 应答时传入本地初始mixedStream currentCall.answer(mixedStream) bindCallEvents(currentCall) }) // 抽离公共的事件绑定逻辑 const bindCallEvents = (callInstance) => { // 保留初始流接收逻辑 callInstance.on("stream",remoteStream=>{ const remoteVideo = document.getElementById("remote-video") remoteVideo.srcObject = remoteStream remoteVideo.play() }) // 新增:监听通话过程中新增的轨道,手动追加到已有的远端流中 callInstance.on("track", (track, remoteStream) => { remoteStream.addTrack(track) }) }
3. 实现中途新增视频轨逻辑
新增视频轨时需要完成本地流更新、加轨、重协商三个步骤,全程不需要挂断重建通话:
const addVideoTrackMidCall = async () => { if(!currentCall || !remotePeerConnection) return try{ // 开摄像头用getUserMedia,共享屏幕替换为getDisplayMedia即可 videoStream = await navigator.mediaDevices.getUserMedia({video:true}) // videoStream = await navigator.mediaDevices.getDisplayMedia({video:true}) } catch(err){ console.error('获取视频流失败:', err) return } const videoTrack = videoStream.getVideoTracks()[0] // 将新视频轨加入本地混合流,同步本地预览 mixedStream.addTrack(videoTrack) // 加轨时必须传入关联的mixedStream,否则对端无法正确归类轨道 remotePeerConnection.addTrack(videoTrack, mixedStream) // 手动触发重协商,将新的媒体能力同步给对端,PeerJS会自动转发协商信令 try { const offer = await remotePeerConnection.createOffer() await remotePeerConnection.setLocalDescription(offer) } catch (err) { console.error('重协商失败:', err) } // 监听轨道结束事件(比如用户停止共享屏幕),自动清理视频轨 videoTrack.onended = () => { removeVideoTrackMidCall() } }
4. 配套视频轨移除逻辑(可选)
如果需要支持中途关闭视频/停止共享,可使用以下逻辑,同样不需要挂断通话:
const removeVideoTrackMidCall = () => { if(!currentCall || !remotePeerConnection) return const videoSender = remotePeerConnection.getSenders().find(s => s.track?.kind === 'video') if(videoSender){ remotePeerConnection.removeTrack(videoSender) // 清理本地视频轨 const localVideoTrack = mixedStream.getVideoTracks()[0] if(localVideoTrack){ localVideoTrack.stop() mixedStream.removeTrack(localVideoTrack) } // 触发重协商同步状态 remotePeerConnection.createOffer() .then(offer => remotePeerConnection.setLocalDescription(offer)) .catch(err => console.error('重协商失败:', err)) } if(videoStream){ videoStream.getTracks().forEach(t => t.stop()) videoStream = null } }
注意事项
- 主叫、被叫两端都必须绑定
track事件,否则无法接收对方中途新增的轨道 addTrack方法的第二个参数必须传入当前通话使用的mixedStream,不能省略- 不需要手动编写重协商的信令转发逻辑,PeerJS内部已经封装了offer/answer的自动转发能力,仅需在本地修改轨道后调用
setLocalDescription即可触发协商流程
内容的提问来源于stack exchange,提问作者Ahmed Ghazi
相关产品推荐
相关产品推荐

