使用WebRTC API解复用RTP音频流
嘿,刚好碰到过类似的场景,我来给你唠唠怎么用WebRTC API搞定RTP音频流的解复用问题~
你现在做的是那种服务器转发RTP包的语音聊天应用对吧?每个客户端都有自己唯一的SSRC,而且目前的代码已经能让所有音频流正常播放了,这其实是因为WebRTC在底层已经帮你做了不少工作,但如果需要更精细的控制,或者想搞明白背后的逻辑,咱们可以拆解来看:
为啥现有代码能正常工作?
WebRTC的RTCPeerConnection在接收到RTP包时,会自动根据包里面的SSRC字段来区分不同的媒体源,把来自不同客户端的音频流分别封装成MediaStreamTrack,然后通过ontrack事件推送给你。所以你的ontrack回调里每次收到的ev.track,其实就是某一个客户端的独立音频轨道,直接把它加到MediaStream里就能播放啦。
怎么手动区分/解复用流?
如果需要对不同客户端的流做自定义处理(比如标记说话者、过滤特定流、自定义混音),咱们可以从这两个方向入手:
1. 利用WebRTC自带的轨道和接收器信息
在ontrack事件里,你可以通过接收器的参数拿到对应流的SSRC,以此来标识不同的客户端:
function join_vc(offer) { const rtc_conn = new RTCPeerConnection({ bundlePolicy: 'max-compat' }); rtc_conn.ontrack = (ev) => { // 拿到当前轨道对应的SSRC,用来区分不同客户端 const ssrc = ev.receiver.rtpParameters.encodings[0].ssrc; console.log(`收到来自客户端(SSRC: ${ssrc})的音频流`); // 创建媒体流并播放 const audioStream = new MediaStream([ev.track]); const audioElement = document.createElement('audio'); audioElement.srcObject = audioStream; audioElement.volume = 1; // 可以给audioElement加个标识,比如id设为SSRC值,方便后续管理 audioElement.id = `audio-${ssrc}`; document.body.appendChild(audioElement); audioElement.play().catch(err => { console.error(`播放SSRC: ${ssrc}的音频失败:`, err); }); }; // 处理SDP协商的逻辑 rtc_conn.setRemoteDescription(offer) .then(() => rtc_conn.createAnswer()) .then(answer => rtc_conn.setLocalDescription(answer)) .catch(err => console.error('连接协商出错:', err)); }
这种方式最省心,完全依赖WebRTC的自动处理,只需要在收到轨道时标记SSRC就行。
2. 手动处理RTP包(自定义解复用)
如果需要更底层的控制,比如自己解析RTP包、过滤特定包,就可以用RTCRtpScriptTransform把RTP包传到Worker里处理,在Worker里根据SSRC来解复用:
首先修改主进程的代码,给音频接收器添加变换:
function join_vc(offer) { const rtc_conn = new RTCPeerConnection({ bundlePolicy: 'max-compat' }); // 先添加一个仅接收的音频收发器 rtc_conn.addTransceiver('audio', { direction: 'recvonly' }); // 获取音频接收器,添加RTP变换 setTimeout(() => { const audioReceiver = rtc_conn.getReceivers().find(recv => recv.track.kind === 'audio'); if (audioReceiver) { // 创建一个Worker来处理RTP包 const rtpTransform = new RTCRtpScriptTransform(new Worker('rtp-demux-worker.js')); audioReceiver.transform = rtpTransform; } }, 100); rtc_conn.ontrack = (ev) => { const ssrc = ev.receiver.rtpParameters.encodings[0].ssrc; console.log(`收到SSRC: ${ssrc}的音频轨道`); const audioStream = new MediaStream([ev.track]); const audioElement = document.createElement('audio'); audioElement.srcObject = audioStream; audioElement.id = `audio-${ssrc}`; document.body.appendChild(audioElement); audioElement.play().catch(err => console.error(`播放失败:`, err)); }; // SDP协商逻辑 rtc_conn.setRemoteDescription(offer) .then(() => rtc_conn.createAnswer()) .then(answer => rtc_conn.setLocalDescription(answer)) .catch(err => console.error('连接出错:', err)); }
然后创建rtp-demux-worker.js文件,在Worker里处理RTP包的解复用:
// 监听RTP包事件 self.onrtp = (event) => { const rtpPacket = event.rtp; // 从RTP头里拿到SSRC const packetSSRC = rtpPacket.ssrc; // 这里可以自定义逻辑:比如只保留特定SSRC的包,或者给包打标记 console.log(`收到RTP包,SSRC: ${packetSSRC}`); // 处理完后把包发回给WebRTC继续处理 event.sendBack(rtpPacket); };
这种方式适合需要对RTP包做自定义处理的场景,比如过滤掉某个客户端的流,或者在转发前修改包内容。
关键注意点
- 服务器转发RTP包的时候一定不要修改包的SSRC,这是WebRTC区分不同流的核心标识,改了就会导致解复用失败。
- 每个
MediaStreamTrack对应一个SSRC,也就是一个客户端的音频流,你可以把多个轨道加到同一个MediaStream里混音播放,也可以单独播放每个轨道。
备注:内容来源于stack exchange,提问作者Inexspectatus somnium

