Skip to the content.

← 返回首页

WebRTC 项目技术说明(学习版)

本目录用于记录 WebRTC 示例项目的学习向文档,帮助你从整体把握项目架构、信令流程和前端实现。

项目定位:本仓库是一个基于 Go 的最小可用 WebRTC Demo,用于学习一对一通话、信令、媒体控制、DataChannel、录制和简单房间成员管理。


1. 项目整体架构

1.1 模块结构

WebRTC/
├── cmd/
│   └── server/          # HTTP + WebSocket 服务入口
├── internal/
│   └── signal/          # 信令逻辑(房间管理、消息转发)
├── web/                 # 浏览器前端 Demo(纯 HTML + JS)
│   ├── index.html
│   └── app.js
├── docs/                # 文档(当前目录)
├── go.mod
└── README.md

1.2 高层交互示意

graph TD
  subgraph BrowserA[浏览器 A]
    AUI[HTML UI]
    AJS[app.js]
  end

  subgraph BrowserB[浏览器 B]
    BUI[HTML UI]
    BJS[app.js]
  end

  subgraph Server[Go 后端]
    HTTP[HTTP 静态文件]
    WS["/ws WebSocket"]
    HUB[Signal Hub]
  end

  AUI --> AJS
  BUI --> BJS

  AJS -- HTTP GET / --> HTTP
  BJS -- HTTP GET / --> HTTP

  AJS -- WebSocket /ws --> WS
  BJS -- WebSocket /ws --> WS
  WS --> HUB

  AJS -- WebRTC 媒体/数据通道 --> BJS

2. 信令服务器与房间管理

2.1 Message 结构

文件:internal/signal/message.go

type Message struct {
    Type      string          `json:"type"`
    Room      string          `json:"room"`
    From      string          `json:"from"`
    To        string          `json:"to,omitempty"`
    SDP       json.RawMessage `json:"sdp,omitempty"`
    Candidate json.RawMessage `json:"candidate,omitempty"`
    Members   []string        `json:"members,omitempty"`
}

可选的辅助消息:

2.2 Hub 与 Client

文件:internal/signal/hub.go

2.3 一对一通话信令时序

sequenceDiagram
    participant A as 浏览器 A
    participant B as 浏览器 B
    participant S as Signal Hub

    A->>S: WebSocket /ws
    A->>S: {type: "join", room, from: A}
    S-->>A: room_members (仅 A)

    B->>S: WebSocket /ws
    B->>S: {type: "join", room, from: B}
    S-->>A: room_members (A, B)
    S-->>B: room_members (A, B)

    A->>S: offer (to: B)
    S-->>B: offer
    B->>S: answer (to: A)
    S-->>A: answer

    A->>S: candidate (to: B)
    S-->>B: candidate
    B->>S: candidate (to: A)
    S-->>A: candidate

2.4 房间成员列表广播逻辑


3. 前端整体流程与状态机

文件:web/app.js

3.1 核心状态变量

3.2 状态机

stateDiagram-v2
    [*] --> Idle
    Idle: 未连接房间
    Joined: 已加入房间
    Calling: 通话中
    Ended: 通话结束

    Idle --> Joined: 点击 Join 且成功连接 & getUserMedia
    Joined --> Calling: 点击 Call 并成功发起呼叫
    Calling --> Joined: 点击 Hangup 结束通话
    Joined --> Idle: WebSocket 关闭 / 刷新页面

4. 媒体获取与控制

4.1 获取本地媒体 getMedia()

localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
document.getElementById('local').srcObject = localStream

4.2 将媒体添加到 RTCPeerConnection

ensurePC 中:

await getMedia()
localStream.getTracks().forEach(t => pc.addTrack(t, localStream))
pc.ontrack = (e) => {
  const video = ensureRemoteTile(peerId)
  if (video) video.srcObject = e.streams[0]
}

4.3 静音 / 恢复麦克风

const tracks = localStream.getAudioTracks()
muted = !muted
tracks.forEach(t => { t.enabled = !muted })

4.4 摄像头开关

const tracks = localStream.getVideoTracks()
cameraOff = !cameraOff
tracks.forEach(t => { t.enabled = !cameraOff })

4.5 屏幕共享与 replaceTrack

  1. 调用 getDisplayMedia 获取屏幕流:

    const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })
    const track = stream.getVideoTracks()[0]
    
  2. 将本地 <video> 源切换为屏幕:

    localVideo.srcObject = screenStream
    
  3. 若已建立 pc,找到现有发送视频的 RTCRtpSender,替换其中的 track

    for (const peer of peers.values()) {
      const sender = peer.pc.getSenders().find(s => s.track && s.track.kind === 'video')
      if (sender) await sender.replaceTrack(track)
    }
    
  4. 当用户停止共享(浏览器自带“停止共享”按钮):

    • 停掉屏幕共享所有 track;
    • 恢复 localStream 到本地 <video>
    • 再次用 replaceTrack 把发送的 track 换回摄像头。

5. DataChannel 文本聊天

5.1 创建与接收

const dc = peer.pc.createDataChannel('chat')
setupDataChannel(peerId, dc)
pc.ondatachannel = (e) => {
  setupDataChannel(peerId, e.channel)
}

5.2 事件处理与 UI

setupDataChannel 中:

function setupDataChannel(peerId, dc) {
  const peer = peers.get(peerId)
  if (peer) peer.dc = dc
  dc.onopen = () => appendChat('[system] chat channel opened: ' + peerId)
  dc.onmessage = (e) => appendChat(peerId + ': ' + e.data)
  dc.onclose = () => appendChat('[system] chat channel closed: ' + peerId)
}

发送消息:

chatSend.onclick = () => {
  const text = chatInput.value.trim()
  if (!text) return
  const channels = []
  for (const peer of peers.values()) {
    if (peer.dc && peer.dc.readyState === 'open') channels.push(peer.dc)
  }
  if (!channels.length) {
    setError('聊天通道未建立(请先 Call)')
    return
  }
  channels.forEach(dc => dc.send(text))
  appendChat('me: ' + text)
  chatInput.value = ''
}

6. 本地录制与下载

6.1 选择录制对象

函数 getRecordStream()

  1. 若存在任意 peer 的 remoteStream,优先录制远端流;
  2. 否则若正在屏幕共享,录制 screenStream
  3. 否则,如果有 localStream,录制本地流;
  4. 否则提示“没有可录制的媒体流”。

6.2 使用 MediaRecorder


7. 房间成员列表

7.1 服务端广播 room_members

见第 2.4 节:每次加入/离开房间都会广播一条成员列表消息给该房间所有客户端。

7.2 前端渲染与点击选人


8. 建议的阅读与实践顺序

如果你想系统学习这个 Demo,可以按下面顺序:

  1. 通读本文件的第 1~3 节
    • 熟悉整体架构、信令流程、前端状态机。
  2. 结合代码查看:
    • internal/signal/hub.go:对照第 2 节理解房间管理与消息转发;
    • web/app.js:对照第 3~7 节理解状态管理、媒体控制、DataChannel 和录制。
  3. 按 ROADMAP 实践扩展:
    • 尝试小规模 Mesh 多人通话;
    • 尝试简单 TURN/HTTPS 或 Docker 打包。

你可以在 docs/ 目录继续添加更细的文档,例如: