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

如何使用WebRTC将前端摄像头直播流传输至后端(非P2P场景)

可以用WebRTC实现客户端到服务器的直播流传输(无需P2P)

WebRTC并非只能用于点对点通信,它完全支持客户端与服务器之间的直接媒体流传输——这种场景通常通过**WebRTC SFU(选择性转发单元)**或直接建立客户端-服务器的PeerConnection实现,刚好匹配你的需求:将前端处理后的视频流实时传到后端,完成叠加层、转码后再推流到YouTube等平台。

核心思路

你需要在后端搭建一个WebRTC媒体服务(或实现基础的WebRTC服务器逻辑),前端通过PeerConnection直接与服务器建立连接,将捕获并处理后的媒体流发送给服务器,而非其他客户端。

具体实现步骤

1. 前端:捕获并叠加处理视频流

先通过getUserMedia获取摄像头流,再用<canvas>添加实时叠加层,最后从canvas导出处理后的媒体流:

// 捕获摄像头音视频流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(async (rawStream) => {
    const sourceVideo = document.getElementById('source-video');
    sourceVideo.srcObject = rawStream;
    await sourceVideo.play();

    // 初始化Canvas用于叠加层绘制
    const overlayCanvas = document.getElementById('overlay-canvas');
    const ctx = overlayCanvas.getContext('2d');
    overlayCanvas.width = sourceVideo.videoWidth;
    overlayCanvas.height = sourceVideo.videoHeight;

    // 实时绘制视频+自定义叠加层
    function renderFrame() {
      // 绘制原始视频帧
      ctx.drawImage(sourceVideo, 0, 0, overlayCanvas.width, overlayCanvas.height);
      // 添加示例叠加层:文字水印
      ctx.fillStyle = '#ffffff';
      ctx.font = '28px sans-serif';
      ctx.fillText('我的直播水印', 30, 50);
      requestAnimationFrame(renderFrame);
    }
    renderFrame();

    // 从Canvas获取处理后的媒体流(30fps)
    const processedStream = overlayCanvas.captureStream(30);
    // 建立WebRTC连接并发送流到后端
    await connectToServer(processedStream);
  })
  .catch(err => console.error('流捕获失败:', err));

2. 前端:建立与服务器的WebRTC连接

通过WebSocket交换SDP和ICE候选,直接与服务器建立媒体连接:

async function connectToServer(stream) {
  const pc = new RTCPeerConnection({
    // 配置公共STUN服务器,用于NAT穿透
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
  });

  // 将处理后的音视频轨道添加到PeerConnection
  stream.getTracks().forEach(track => pc.addTrack(track, stream));

  // 监听ICE候选,发送到后端
  pc.onicecandidate = (event) => {
    if (event.candidate) {
      sendWebSocketMessage({
        type: 'ice-candidate',
        candidate: event.candidate
      });
    }
  };

  // 创建Offer并设置本地描述
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  // 发送Offer到后端
  sendWebSocketMessage({ type: 'offer', sdp: pc.localDescription });

  // 监听后端返回的消息
  setupWebSocketListener(pc);
}

// 简易WebSocket通信封装
function sendWebSocketMessage(data) {
  const ws = new WebSocket('ws://your-backend-addr:8080/webrtc');
  ws.onopen = () => ws.send(JSON.stringify(data));
}

function setupWebSocketListener(pc) {
  const ws = new WebSocket('ws://your-backend-addr:8080/webrtc');
  ws.onmessage = async (event) => {
    const msg = JSON.parse(event.data);
    switch (msg.type) {
      case 'answer':
        await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
        break;
      case 'ice-candidate':
        await pc.addIceCandidate(new RTCIceCandidate(msg.candidate));
        break;
    }
  };
}

3. 后端:实现WebRTC服务器逻辑

后端需要处理WebSocket消息(接收Offer、返回Answer、交换ICE候选),并接收前端传来的媒体流。这里用Go语言的pion/webrtc库做极简示例:

package main

import (
  "encoding/json"
  "fmt"
  "net/http"
  "github.com/pion/webrtc/v3"
)

func main() {
  // WebRTC配置
  config := webrtc.Configuration{
    ICEServers: []webrtc.ICEServer{
      {URLs: []string{"stun:stun.l.google.com:19302"}},
    },
  }

  // 处理WebSocket连接
  http.HandleFunc("/webrtc", func(w http.ResponseWriter, r *http.Request) {
    upgrader := http.Upgrader{CheckOrigin: func(_ *http.Request) bool { return true }}
    wsConn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
      fmt.Println("WebSocket升级失败:", err)
      return
    }

    // 创建服务器端PeerConnection
    pc, err := webrtc.NewPeerConnection(config)
    if err != nil {
      fmt.Println("PeerConnection创建失败:", err)
      return
    }

    // 监听前端传来的媒体轨道
    pc.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
      fmt.Printf("收到媒体轨道: %s\n", track.Codec().MimeType)
      // 这里可以对接FFmpeg/GStreamer,完成叠加层二次处理、转码后推流到YouTube
      // 示例:将流写入本地文件(实际场景替换为RTMP推流逻辑)
      // file, _ := os.Create("live-stream.raw")
      // io.Copy(file, track)
    })

    // 监听ICE候选,发送给前端
    pc.OnICECandidate(func(c *webrtc.ICECandidate) {
      if c == nil { return }
      candidateJSON, _ := json.Marshal(map[string]any{
        "type": "ice-candidate",
        "candidate": c.ToJSON(),
      })
      wsConn.WriteMessage(1, candidateJSON)
    })

    // 处理前端消息
    for {
      _, msgBytes, err := wsConn.ReadMessage()
      if err != nil {
        fmt.Println("WebSocket读取失败:", err)
        return
      }

      var msg map[string]any
      json.Unmarshal(msgBytes, &msg)

      switch msg["type"] {
      case "offer":
        sdp := msg["sdp"].(map[string]any)
        offer := webrtc.SessionDescription{
          Type: webrtc.SDPTypeOffer,
          SDP:  sdp["sdp"].(string),
        }
        pc.SetRemoteDescription(offer)

        // 创建Answer并返回给前端
        answer, _ := pc.CreateAnswer(nil)
        pc.SetLocalDescription(answer)
        answerJSON, _ := json.Marshal(map[string]any{
          "type": "answer",
          "sdp": answer,
        })
        wsConn.WriteMessage(1, answerJSON)
      case "ice-candidate":
        candidate := msg["candidate"].(map[string]any)
        pc.AddICECandidate(webrtc.ICECandidateInit{
          Candid: candidate["candidate"].(string),
          SDPMLineIndex: candidate["sdpMLineIndex"].(float64),
          SDPMid: candidate["sdpMid"].(string),
        })
      }
    }
  })

  fmt.Println("服务器运行在 :8080")
  http.ListenAndServe(":8080", nil)
}

关键注意事项

  • 叠加层处理:建议在前端用Canvas完成,后端处理会增加延迟,前端处理更符合直播的实时性要求。
  • 流转码与推流:后端收到WebRTC流后,需用FFmpeg或GStreamer将其转码为RTMP/HLS格式,再推送到YouTube等平台的直播地址。
  • NAT穿透:如果服务器部署在公网外,需要配置TURN服务器(可自行搭建或使用公共服务),确保客户端能穿透NAT建立连接。

内容的提问来源于stack exchange,提问作者Safyan Yaqoob

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 21:35:57