WebRTC非P2P模式下向Node.js实时传输音视频流的实现咨询
实现WebRTC媒体流发送到Node.js服务器(非P2P模式)
嗨,我来帮你搞定这个需求!你要做的其实就是把WebRTC的媒体流发送到Node.js服务器(相当于一个简易的SFU/媒体服务器),而不是直接发给其他对等端,这样就能同时实现存储和广播功能了。下面我分前端和后端两部分给你详细说明:
一、前端:将媒体流发送到Node.js服务器
你已经熟悉P2P的WebRTC流程,这里的核心区别是把PeerConnection的对等端换成Node.js服务器,配合WebSocket做信令交换:
建立WebSocket信令通道
首先用WebSocket和Node.js服务器建立连接,用来交换SDP Offer/Answer和ICE候选:const ws = new WebSocket('ws://your-node-server:8080');获取并配置本地媒体流
按照你的需求设置码率和帧率,注意有些浏览器可能需要额外通过RTCRtpSender调整参数:const stream = await navigator.mediaDevices.getUserMedia({ video: { bitrate: 125000, // 125 kbps frameRate: { ideal: 10, max: 12 } }, audio: true }); // 可选:强制设置编码器参数(部分浏览器需要) const setMediaParams = async (pc) => { const senders = pc.getSenders(); for (const sender of senders) { const params = sender.getParameters(); if (sender.track.kind === 'video') { params.encodings[0].maxBitrate = 125000; params.encodings[0].frameRate = 10; } await sender.setParameters(params); } };创建RTCPeerConnection并连接到服务器
初始化PeerConnection,添加本地流轨道,然后发起Offer:const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); // 添加本地媒体轨道到PeerConnection stream.getTracks().forEach(track => pc.addTrack(track, stream)); // 监听ICE候选,发送给服务器 pc.onicecandidate = (e) => { if (e.candidate) { ws.send(JSON.stringify({ type: 'ice-candidate', candidate: e.candidate })); } }; // 创建Offer并发送给服务器 const offer = await pc.createOffer(); await pc.setLocalDescription(offer); ws.send(JSON.stringify({ type: 'offer', sdp: offer.sdp })); // 接收服务器的Answer并设置远程描述 ws.onmessage = async (e) => { const data = JSON.parse(e.data); if (data.type === 'answer') { await pc.setRemoteDescription(new RTCSessionDescription(data)); } else if (data.type === 'ice-candidate') { await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } }; // 可选:设置媒体参数 await setMediaParams(pc);
二、Node.js后端:接收、存储和广播媒体流
后端需要用到两个核心库:wrtc(Node.js的WebRTC实现)和ws(WebSocket服务器),另外可以用fluent-ffmpeg处理媒体存储。
安装依赖
npm install wrtc ws fluent-ffmpeg搭建WebSocket+WebRTC服务器
维护客户端连接的PeerConnection列表,处理信令交换,同时接收媒体流:const WebSocket = require('ws'); const wrtc = require('wrtc'); const ffmpeg = require('fluent-ffmpeg'); const fs = require('fs'); const wss = new WebSocket.Server({ port: 8080 }); const peerConnections = new Set(); // 存储所有客户端的PeerConnection wss.on('connection', (ws) => { const pc = new wrtc.RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); peerConnections.add(pc); // 接收媒体流轨道 pc.ontrack = (e) => { const stream = e.streams[0]; // 1. 存储媒体流到文件 saveStreamToFile(stream); // 2. 广播流给其他客户端 broadcastStream(stream, pc); }; // 发送ICE候选给客户端 pc.onicecandidate = (e) => { if (e.candidate) { ws.send(JSON.stringify({ type: 'ice-candidate', candidate: e.candidate })); } }; // 处理客户端发来的信令 ws.on('message', async (data) => { const msg = JSON.parse(data); if (msg.type === 'offer') { await pc.setRemoteDescription(new wrtc.RTCSessionDescription(msg)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); ws.send(JSON.stringify({ type: 'answer', sdp: answer.sdp })); } else if (msg.type === 'ice-candidate') { await pc.addIceCandidate(new wrtc.RTCIceCandidate(msg.candidate)); } }); // 连接关闭时清理资源 ws.on('close', () => { peerConnections.delete(pc); pc.close(); }); }); // 存储媒体流到文件(用ffmpeg转码为MP4) function saveStreamToFile(stream) { const outputPath = `./recording-${Date.now()}.mp4`; const mediaStream = new wrtc.MediaStream([...stream.getTracks()]); const readableStream = mediaStream.toReadableStream(); ffmpeg(readableStream) .inputFormat('webm') .output(outputPath) .videoCodec('libx264') .audioCodec('aac') .on('end', () => console.log(`Recording saved to ${outputPath}`)) .on('error', (err) => console.error('FFmpeg error:', err)) .run(); } // 广播媒体流给其他客户端 function broadcastStream(stream, excludePc) { stream.getTracks().forEach(track => { peerConnections.forEach(pc => { if (pc !== excludePc) { pc.addTrack(track.clone(), stream); } }); }); }
三、关键注意事项
- NAT穿透:确保配置了STUN/TURN服务器,如果服务器在公网,STUN基本足够;如果在局域网内,可能需要TURN。
- 性能优化:因为要同时处理存储和广播,服务器需要足够的CPU和带宽,尤其是当广播给大量用户时,可以考虑用更成熟的SFU框架(比如mediasoup)来替代简易实现。
- 编码兼容性:前端设置的码率和帧率可能在不同浏览器有差异,建议测试主流浏览器(Chrome、Firefox)的兼容性。
内容的提问来源于stack exchange,提问作者Syed Sajid




