You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

WebRTC非P2P模式下向Node.js实时传输音视频流的实现咨询

实现WebRTC媒体流发送到Node.js服务器(非P2P模式)

嗨,我来帮你搞定这个需求!你要做的其实就是把WebRTC的媒体流发送到Node.js服务器(相当于一个简易的SFU/媒体服务器),而不是直接发给其他对等端,这样就能同时实现存储和广播功能了。下面我分前端和后端两部分给你详细说明:

一、前端:将媒体流发送到Node.js服务器

你已经熟悉P2P的WebRTC流程,这里的核心区别是把PeerConnection的对等端换成Node.js服务器,配合WebSocket做信令交换:

  1. 建立WebSocket信令通道
    首先用WebSocket和Node.js服务器建立连接,用来交换SDP Offer/Answer和ICE候选:

    const ws = new WebSocket('ws://your-node-server:8080');
    
  2. 获取并配置本地媒体流
    按照你的需求设置码率和帧率,注意有些浏览器可能需要额外通过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);
      }
    };
    
  3. 创建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处理媒体存储。

  1. 安装依赖

    npm install wrtc ws fluent-ffmpeg
    
  2. 搭建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

火山引擎 最新活动