P2P视频传输完全指南:WebRTC从原理到优化

目录

  1. P2P视频传输基础
  2. WebRTC架构深度解析
  3. NAT穿透原理详解
  4. 信令服务器实现
  5. WebRTC媒体流处理
  6. 完整端到端实现
  7. 5个行业应用案例
  8. 性能优化实战
  9. 故障排查与监控
  10. P2P vs 中继对比

一句话总结

P2P(Peer-to-Peer)视频传输通过WebRTC技术实现设备间的直连通信,利用STUN/TURN服务器穿透NAT,无需中转服务器即可传输音视频流,降低延迟、节省带宽成本。


核心概念图

P2P视频传输完整流程

┌────────────────── 客户端A ──────────────────┐
│  1. 获取本地媒体流                          │
│     getUserMedia() → Video/Audio           │
│                                             │
│  2. 创建PeerConnection                     │
│     new RTCPeerConnection()                │
│                                             │
│  3. 创建Offer SDP                          │
│     createOffer() → SDP Description        │
└─────────────────────┬───────────────────────┘
                      │
                      ↓ 通过信令服务器交换SDP
┌──────────────────────────────────────────────┐
│         信令服务器(WebSocket)              │
│  - 转发SDP Offer/Answer                     │
│  - 转发ICE Candidate                        │
│  - 不传输媒体数据                           │
└──────────────────────┬───────────────────────┘
                      │
                      ↓
┌────────────────── 客户端B ──────────────────┐
│  4. 接收Offer,创建Answer                   │
│     setRemoteDescription()                  │
│     createAnswer() → SDP Description        │
│                                             │
│  5. ICE候选者收集                           │
│     onicecandidate → 收集IP/端口            │
└─────────────────────┬───────────────────────┘
                      │
                      ↓
┌────────────── STUN/TURN服务器 ──────────────┐
│  6. NAT穿透                                 │
│     STUN: 获取公网IP和端口                  │
│     TURN: P2P失败时中继数据                 │
└─────────────────────┬───────────────────────┘
                      │
                      ↓ 建立P2P连接
┌──────────────────────────────────────────────┐
│             P2P直连通道                      │
│                                              │
│  客户端A ←──────────────────→ 客户端B       │
│          RTP/RTCP 媒体流                    │
│                                              │
│  特点:                                      │
│  - 直接传输,无需服务器中转                 │
│  - 延迟低(<100ms)                         │
│  - 节省服务器带宽                           │
└──────────────────────────────────────────────┘

NAT穿透成功率:
- Symmetric NAT  ↔ Symmetric NAT: 10%(需TURN)
- Cone NAT       ↔ Symmetric NAT: 70%(STUN)
- Cone NAT       ↔ Cone NAT:      95%(STUN)

1. P2P视频传输基础

1.1 什么是P2P视频传输?

P2P(Peer-to-Peer) 是一种点对点的网络通信模式,设备之间直接连接,无需通过中心服务器中转数据。

传统C/S模式 vs P2P模式:

C/S模式(Client-Server):
客户端A → 服务器 → 客户端B

缺点:
- 服务器带宽压力大
- 延迟高(两跳)
- 成本高(服务器流量费)

P2P模式:
客户端A ←─────→ 客户端B

优点:
- 直接连接,延迟低
- 节省服务器带宽
- 可扩展性强

典型应用场景:

  1. 视频通话:微信视频、Zoom、Skype
  2. 智能硬件:智慧屏远程预览、IP摄像头查看
  3. 远程桌面:TeamViewer、向日葵
  4. 文件传输:P2P文件共享、AirDrop
  5. 游戏对战:P2P联机游戏

1.2 为什么需要WebRTC?

WebRTC(Web Real-Time Communication) 是Google开源的实时通信框架,提供了P2P音视频传输的完整解决方案。

核心特点:

特点说明优势
NAT穿透自动STUN/TURN90%成功率
加密传输DTLS-SRTP端到端加密
自适应码率根据网络调整流畅播放
跨平台Web/Android/iOS统一API
开源免费Apache 2.0零成本

WebRTC vs 传统流媒体:

维度WebRTC(P2P)RTMP/HLS(C/S)
延迟<100ms1-5秒
连接方式P2P直连服务器中转
带宽成本低(P2P分担)高(服务器承担)
NAT穿透自动不需要
浏览器支持原生支持需插件

1.3 WebRTC技术栈

┌───────────────────────────────────────┐
│          应用层                       │
│  getUserMedia / RTCPeerConnection     │
└───────────────────────────────────────┘
              ↓
┌───────────────────────────────────────┐
│       JavaScript API层                │
│  - 媒体采集                           │
│  - 连接管理                           │
│  - 数据通道                           │
└───────────────────────────────────────┘
              ↓
┌───────────────────────────────────────┐
│       WebRTC Core(C++)              │
│  ┌─────────────────────────────────┐ │
│  │  音频处理引擎                   │ │
│  │  - 回声消除(AEC)              │ │
│  │  - 噪声抑制(NS)               │ │
│  │  - 自动增益(AGC)              │ │
│  └─────────────────────────────────┘ │
│  ┌─────────────────────────────────┐ │
│  │  视频处理引擎                   │ │
│  │  - VP8/VP9/H.264编解码          │ │
│  │  - 分辨率适配                   │ │
│  │  - 帧率控制                     │ │
│  └─────────────────────────────────┘ │
│  ┌─────────────────────────────────┐ │
│  │  网络传输层                     │ │
│  │  - STUN/TURN/ICE                │ │
│  │  - DTLS-SRTP加密                │ │
│  │  - 拥塞控制(GCC)              │ │
│  └─────────────────────────────────┘ │
└───────────────────────────────────────┘
              ↓
┌───────────────────────────────────────┐
│         网络层(UDP/TCP)             │
└───────────────────────────────────────┘

2. WebRTC架构深度解析

2.1 核心组件

1. RTCPeerConnection:

P2P连接的核心类,负责:

  • ICE候选者收集
  • SDP协商
  • 媒体流传输
  • 数据通道

2. getUserMedia:

获取本地音视频流:

navigator.mediaDevices.getUserMedia({
    video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 }
    },
    audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
    }
}).then(stream => {
    // 获取到本地流
    localVideo.srcObject = stream;
});

3. RTCDataChannel:

P2P数据通道(非音视频):

const dataChannel = peerConnection.createDataChannel('chat');

dataChannel.onopen = () => {
    dataChannel.send('Hello!');
};

dataChannel.onmessage = (event) => {
    console.log('Received:', event.data);
};

2.2 SDP协商流程

SDP(Session Description Protocol) 描述了媒体会话的参数(编解码器、分辨率、网络地址等)。

完整SDP协商流程:

Offer-Answer模型:

步骤1:Caller创建Offer
┌────────────────────────────┐
│ createOffer()              │
│  ↓                         │
│ SDP Offer:                 │
│  v=0                       │
│  m=video 9 UDP/TLS/RTP/... │
│  a=rtpmap:96 VP8/90000     │
│  a=candidate:...           │
└────────────────────────────┘
              ↓ 通过信令服务器发送
┌────────────────────────────┐
│ Callee收到Offer             │
│  ↓                         │
│ setRemoteDescription()     │
│  ↓                         │
│ createAnswer()             │
│  ↓                         │
│ SDP Answer:                │
│  v=0                       │
│  m=video 9 UDP/TLS/RTP/... │
│  a=rtpmap:96 VP8/90000     │
│  a=candidate:...           │
└────────────────────────────┘
              ↓ 通过信令服务器发送回
┌────────────────────────────┐
│ Caller收到Answer            │
│  ↓                         │
│ setRemoteDescription()     │
│  ↓                         │
│ 开始ICE连接检查             │
└────────────────────────────┘

SDP示例:

v=0
o=- 1234567890 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS stream

m=video 9 UDP/TLS/RTP/SAVPF 96 97
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:abc123
a=ice-pwd:def456
a=fingerprint:sha-256 AA:BB:CC:...
a=setup:actpass
a=mid:0
a=sendrecv
a=rtcp-mux
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
a=candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host
a=candidate:2 1 UDP 1694498815 8.8.8.8 12345 typ srflx raddr 192.168.1.100 rport 54321

m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:abc123
a=ice-pwd:def456
a=fingerprint:sha-256 AA:BB:CC:...
a=setup:actpass
a=mid:1
a=sendrecv
a=rtcp-mux
a=rtpmap:111 opus/48000/2

2.3 ICE候选者收集

ICE(Interactive Connectivity Establishment) 是WebRTC用于NAT穿透的框架。

候选者类型:

类型说明优先级示例
Host本地IP地址高192.168.1.100:54321
Server Reflexive(srflx)STUN获取的公网IP中8.8.8.8:12345
RelayTURN中继地址低1.2.3.4:56789

ICE候选者收集流程:

// Android WebRTC实现
val peerConnection = peerConnectionFactory.createPeerConnection(
    iceServers,
    object : PeerConnection.Observer {
        override fun onIceCandidate(candidate: IceCandidate) {
            // 收集到ICE候选者
            Log.d("WebRTC", "ICE Candidate: ${candidate.sdp}")

            // 发送给对方
            signalingClient.sendIceCandidate(
                candidate.sdpMid,
                candidate.sdpMLineIndex,
                candidate.sdp
            )
        }

        override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {
            when (state) {
                PeerConnection.IceGatheringState.GATHERING -> {
                    Log.d("WebRTC", "ICE候选者收集中...")
                }
                PeerConnection.IceGatheringState.COMPLETE -> {
                    Log.d("WebRTC", "ICE候选者收集完成")
                }
            }
        }

        override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {
            when (state) {
                PeerConnection.IceConnectionState.CHECKING -> {
                    Log.d("WebRTC", "ICE连接检查中...")
                }
                PeerConnection.IceConnectionState.CONNECTED -> {
                    Log.d("WebRTC", "P2P连接成功!")
                }
                PeerConnection.IceConnectionState.FAILED -> {
                    Log.e("WebRTC", "P2P连接失败")
                }
            }
        }
    }
)

3. NAT穿透原理详解

3.1 NAT类型

NAT(Network Address Translation) 网络地址转换,将内网IP映射到公网IP。

四种NAT类型:

类型特点穿透难度占比
Full Cone任意外部IP可连接极易5%
Restricted Cone仅已通信的外部IP可连接易15%
Port Restricted Cone仅已通信的外部IP:Port可连接中50%
Symmetric每个外部目标分配不同端口难30%

NAT穿透成功率矩阵:

        │ Full  │ Restricted │ Port Restricted │ Symmetric
────────┼───────┼────────────┼─────────────────┼───────────
Full    │ 100%  │    100%    │      100%       │   70%
Restr.  │ 100%  │    100%    │      100%       │   70%
Port R. │ 100%  │    100%    │      95%        │   70%
Symm.   │  70%  │     70%    │      70%        │   10%

结论:
- Symmetric ↔ Symmetric:成功率极低(10%),需TURN中继
- 其他组合:成功率高(70-100%),STUN即可

3.2 STUN服务器原理

STUN(Session Traversal Utilities for NAT) 用于获取设备的公网IP和端口。

STUN工作流程:

客户端(192.168.1.100:54321)
              ↓ UDP请求
┌──────────────────────────────────┐
│         NAT路由器                │
│  映射:192.168.1.100:54321       │
│    →   8.8.8.8:12345            │
└──────────────────────────────────┘
              ↓
┌──────────────────────────────────┐
│       STUN服务器(公网)         │
│  1. 接收请求from 8.8.8.8:12345  │
│  2. 返回响应:                   │
│     Your IP: 8.8.8.8             │
│     Your Port: 12345             │
└──────────────────────────────────┘
              ↓
客户端收到响应,得知公网地址:
  8.8.8.8:12345

然后将此地址作为ICE候选者发送给对方

STUN服务器配置:

val iceServers = listOf(
    PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
    PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
)

免费公共STUN服务器:

stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
stun:stun3.l.google.com:19302
stun:stun4.l.google.com:19302
stun:global.stun.twilio.com:3478

3.3 TURN服务器原理

TURN(Traversal Using Relays around NAT) 当P2P无法直连时,通过服务器中继数据。

TURN工作流程:

客户端A          TURN服务器          客户端B
   │                  │                  │
   │─────分配中继──────→│                  │
   │←────中继地址──────│                  │
   │  1.2.3.4:8080    │                  │
   │                  │                  │
   │                  │←────分配中继──────│
   │                  │────中继地址──────→│
   │                  │  1.2.3.4:8081    │
   │                  │                  │
   │────发送数据──────→│                  │
   │  RTP数据包        │────转发数据──────→│
   │                  │  RTP数据包        │
   │                  │                  │
   │                  │←───发送数据──────│
   │←────转发数据──────│  RTP数据包        │
   │  RTP数据包        │                  │

缺点:
- 消耗服务器带宽(100%)
- 延迟增加(+50ms)
- 成本高(流量费)

TURN服务器部署(Coturn):

# 安装Coturn
sudo apt-get install coturn

# 配置文件 /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349

relay-ip=1.2.3.4      # 服务器公网IP
external-ip=1.2.3.4   # 服务器公网IP

user=username:password
realm=example.com

lt-cred-mech          # 长期凭证机制
cert=/etc/ssl/turn_server_cert.pem
pkey=/etc/ssl/turn_server_pkey.pem

# 启动服务
sudo systemctl start coturn
sudo systemctl enable coturn

TURN服务器配置(客户端):

val iceServers = listOf(
    // STUN
    PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),

    // TURN
    PeerConnection.IceServer.builder("turn:1.2.3.4:3478")
        .setUsername("username")
        .setPassword("password")
        .createIceServer()
)

4. 信令服务器实现

4.1 信令协议设计

信令消息类型:

消息类型说明参数
offerOffer SDPsdp
answerAnswer SDPsdp
ice-candidateICE候选者sdpMid, sdpMLineIndex, candidate
join加入房间roomId
leave离开房间roomId
hangup挂断-

信令服务器(Node.js + WebSocket):

// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// 存储房间和客户端
const rooms = new Map();

wss.on('connection', (ws) => {
    let currentRoom = null;
    let userId = null;

    ws.on('message', (message) => {
        const data = JSON.parse(message);

        switch (data.type) {
            case 'join':
                // 加入房间
                userId = data.userId;
                currentRoom = data.roomId;

                if (!rooms.has(currentRoom)) {
                    rooms.set(currentRoom, new Set());
                }
                rooms.get(currentRoom).add(ws);

                // 通知房间内其他人
                broadcast(currentRoom, {
                    type: 'user-joined',
                    userId: userId
                }, ws);
                break;

            case 'offer':
            case 'answer':
            case 'ice-candidate':
                // 转发信令消息
                broadcast(currentRoom, {
                    type: data.type,
                    userId: userId,
                    ...data
                }, ws);
                break;

            case 'leave':
            case 'hangup':
                // 离开房间
                if (currentRoom && rooms.has(currentRoom)) {
                    rooms.get(currentRoom).delete(ws);
                    broadcast(currentRoom, {
                        type: 'user-left',
                        userId: userId
                    }, ws);
                }
                break;
        }
    });

    ws.on('close', () => {
        // 连接关闭,清理
        if (currentRoom && rooms.has(currentRoom)) {
            rooms.get(currentRoom).delete(ws);
            broadcast(currentRoom, {
                type: 'user-left',
                userId: userId
            }, ws);
        }
    });
});

// 广播消息到房间内所有人(除了发送者)
function broadcast(roomId, message, sender) {
    if (!rooms.has(roomId)) return;

    const clients = rooms.get(roomId);
    clients.forEach(client => {
        if (client !== sender && client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify(message));
        }
    });
}

console.log('Signaling server running on ws://localhost:8080');

4.2 信令客户端(Android)

class SignalingClient(private val serverUrl: String) {
    private var webSocket: WebSocket? = null
    private val okHttpClient = OkHttpClient()
    private var listener: SignalingListener? = null

    interface SignalingListener {
        fun onUserJoined(userId: String)
        fun onUserLeft(userId: String)
        fun onOfferReceived(userId: String, sdp: String)
        fun onAnswerReceived(userId: String, sdp: String)
        fun onIceCandidateReceived(userId: String, sdpMid: String, sdpMLineIndex: Int, candidate: String)
    }

    fun connect(userId: String, roomId: String) {
        val request = Request.Builder()
            .url(serverUrl)
            .build()

        webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.d("Signaling", "Connected to server")

                // 加入房间
                send(JSONObject().apply {
                    put("type", "join")
                    put("userId", userId)
                    put("roomId", roomId)
                })
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                val message = JSONObject(text)
                val type = message.getString("type")

                when (type) {
                    "user-joined" -> {
                        val userId = message.getString("userId")
                        listener?.onUserJoined(userId)
                    }

                    "user-left" -> {
                        val userId = message.getString("userId")
                        listener?.onUserLeft(userId)
                    }

                    "offer" -> {
                        val userId = message.getString("userId")
                        val sdp = message.getString("sdp")
                        listener?.onOfferReceived(userId, sdp)
                    }

                    "answer" -> {
                        val userId = message.getString("userId")
                        val sdp = message.getString("sdp")
                        listener?.onAnswerReceived(userId, sdp)
                    }

                    "ice-candidate" -> {
                        val userId = message.getString("userId")
                        val sdpMid = message.getString("sdpMid")
                        val sdpMLineIndex = message.getInt("sdpMLineIndex")
                        val candidate = message.getString("candidate")
                        listener?.onIceCandidateReceived(userId, sdpMid, sdpMLineIndex, candidate)
                    }
                }
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e("Signaling", "Connection failed", t)
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                Log.d("Signaling", "Connection closed")
            }
        })
    }

    fun sendOffer(sdp: String) {
        send(JSONObject().apply {
            put("type", "offer")
            put("sdp", sdp)
        })
    }

    fun sendAnswer(sdp: String) {
        send(JSONObject().apply {
            put("type", "answer")
            put("sdp", sdp)
        })
    }

    fun sendIceCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String) {
        send(JSONObject().apply {
            put("type", "ice-candidate")
            put("sdpMid", sdpMid)
            put("sdpMLineIndex", sdpMLineIndex)
            put("candidate", candidate)
        })
    }

    private fun send(message: JSONObject) {
        webSocket?.send(message.toString())
    }

    fun disconnect() {
        webSocket?.close(1000, "Client disconnect")
    }

    fun setListener(listener: SignalingListener) {
        this.listener = listener
    }
}

5. WebRTC媒体流处理

5.1 视频编解码

支持的编解码器:

编解码器说明码率延迟质量
VP8Google开源中低中
VP9VP8升级版低低高
H.264硬件加速中低高
AV1最新标准极低高极高

编解码器配置:

// 设置视频编解码器偏好
val constraints = MediaConstraints().apply {
    mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
    mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
}

// VP8编码参数
val videoEncoderFactory = DefaultVideoEncoderFactory(
    eglBase.eglBaseContext,
    true,  // enableIntelVp8Encoder
    true   // enableH264HighProfile
)

// 设置视频编码参数
val videoCodecInfo = VideoCodecInfo("VP8", emptyMap())
val videoEncoder = videoEncoderFactory.createEncoder(videoCodecInfo)

val settings = VideoCodec.Settings(
    1,      // numberOfCores
    1280,   // width
    720,    // height
    1000,   // startBitrate (kbps)
    30,     // maxFramerate
    false   // automaticResizeOn
)

5.2 音频处理

音频处理流程:

原始音频(PCM)
      ↓
回声消除(AEC)
      ↓ 去除回声
噪声抑制(NS)
      ↓ 去除背景噪声
自动增益(AGC)
      ↓ 调整音量
编码(Opus)
      ↓ 压缩
RTP传输

音频处理配置:

val audioConstraints = MediaConstraints().apply {
    // 回声消除
    mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
    mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true"))

    // 噪声抑制
    mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
    mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"))

    // 自动增益
    mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
    mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true"))

    // 高通滤波器
    mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))

    // 音频带宽
    mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false"))
}

val audioSource = peerConnectionFactory.createAudioSource(audioConstraints)
val audioTrack = peerConnectionFactory.createAudioTrack("audio1", audioSource)

5.3 自适应码率(GCC)

GCC(Google Congestion Control) 根据网络状况动态调整码率。

自适应策略:

网络状况检测:
  ↓ 测量丢包率、延迟、带宽
码率调整:
  ├── 网络良好(丢包<5%)→ 增加码率(+10%)
  ├── 网络一般(丢包5-10%)→ 保持码率
  └── 网络拥堵(丢包>10%)→ 降低码率(-20%)

码率范围:
  最低码率:300 kbps
  目标码率:1000 kbps
  最高码率:2000 kbps

码率控制实现:

class BitrateController(
    private val peerConnection: PeerConnection
) {
    private var currentBitrate = 1000  // kbps
    private val minBitrate = 300
    private val maxBitrate = 2000

    fun adjustBitrate(stats: RTCStatsReport) {
        // 获取统计信息
        val packetLossRate = calculatePacketLoss(stats)
        val rtt = calculateRTT(stats)

        // 调整码率
        when {
            packetLossRate < 0.05 && rtt < 100 -> {
                // 网络良好,增加码率
                currentBitrate = (currentBitrate * 1.1).toInt().coerceAtMost(maxBitrate)
            }

            packetLossRate > 0.1 || rtt > 200 -> {
                // 网络拥堵,降低码率
                currentBitrate = (currentBitrate * 0.8).toInt().coerceAtLeast(minBitrate)
            }
        }

        // 应用新码率
        applyBitrate(currentBitrate)
    }

    private fun applyBitrate(bitrate: Int) {
        // 设置编码器码率
        val bitrateParams = RtpParameters.Encoding().apply {
            maxBitrateBps = bitrate * 1000
        }

        // 更新RTP参数
        peerConnection.senders.forEach { sender ->
            val params = sender.parameters
            if (params.encodings.isNotEmpty()) {
                params.encodings[0].maxBitrateBps = bitrate * 1000
                sender.parameters = params
            }
        }

        Log.d("BitrateController", "Bitrate adjusted to $bitrate kbps")
    }
}

6. 完整端到端实现

6.1 完整Android实现

class WebRTCManager(
    private val context: Context,
    private val signalingClient: SignalingClient
) : SignalingClient.SignalingListener {

    private var peerConnectionFactory: PeerConnectionFactory? = null
    private var peerConnection: PeerConnection? = null
    private var localVideoTrack: VideoTrack? = null
    private var localAudioTrack: AudioTrack? = null

    // 初始化
    fun init() {
        // 1. 初始化PeerConnectionFactory
        val initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
            .setEnableInternalTracer(true)
            .createInitializationOptions()
        PeerConnectionFactory.initialize(initOptions)

        // 2. 创建PeerConnectionFactory
        val options = PeerConnectionFactory.Options()
        val eglBase = EglBase.create()

        peerConnectionFactory = PeerConnectionFactory.builder()
            .setOptions(options)
            .setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true))
            .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext))
            .createPeerConnectionFactory()

        // 3. 创建本地媒体流
        createLocalMediaStream()

        // 4. 连接信令服务器
        signalingClient.setListener(this)
        signalingClient.connect("user123", "room456")
    }

    // 创建本地媒体流
    private fun createLocalMediaStream() {
        // 音频
        val audioConstraints = MediaConstraints().apply {
            mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
        }
        val audioSource = peerConnectionFactory!!.createAudioSource(audioConstraints)
        localAudioTrack = peerConnectionFactory!!.createAudioTrack("audio1", audioSource)

        // 视频
        val videoCapturer = createCameraCapturer()
        val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
        videoCapturer.initialize(
            SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext),
            context,
            videoSource.capturerObserver
        )
        videoCapturer.startCapture(1280, 720, 30)

        localVideoTrack = peerConnectionFactory!!.createVideoTrack("video1", videoSource)
    }

    // 创建摄像头采集器
    private fun createCameraCapturer(): VideoCapturer {
        val enumerator = Camera2Enumerator(context)
        val deviceNames = enumerator.deviceNames

        // 优先前置摄像头
        for (deviceName in deviceNames) {
            if (enumerator.isFrontFacing(deviceName)) {
                return enumerator.createCapturer(deviceName, null)
            }
        }

        // 否则后置摄像头
        for (deviceName in deviceNames) {
            if (!enumerator.isFrontFacing(deviceName)) {
                return enumerator.createCapturer(deviceName, null)
            }
        }

        throw IllegalStateException("No camera found")
    }

    // 创建PeerConnection
    private fun createPeerConnection() {
        val iceServers = listOf(
            PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
            PeerConnection.IceServer.builder("turn:1.2.3.4:3478")
                .setUsername("username")
                .setPassword("password")
                .createIceServer()
        )

        peerConnection = peerConnectionFactory!!.createPeerConnection(
            iceServers,
            object : PeerConnection.Observer {
                override fun onIceCandidate(candidate: IceCandidate) {
                    signalingClient.sendIceCandidate(
                        candidate.sdpMid,
                        candidate.sdpMLineIndex,
                        candidate.sdp
                    )
                }

                override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {
                    Log.d("WebRTC", "ICE Connection State: $state")
                }

                override fun onAddStream(stream: MediaStream) {
                    // 收到远端流
                    if (stream.videoTracks.isNotEmpty()) {
                        val remoteVideoTrack = stream.videoTracks[0]
                        remoteVideoTrack.addSink(remoteSurfaceViewRenderer)
                    }
                }

                override fun onRemoveStream(stream: MediaStream) {}
                override fun onDataChannel(dataChannel: DataChannel) {}
                override fun onRenegotiationNeeded() {}
                override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {}
                override fun onSignalingChange(state: PeerConnection.SignalingState) {}
            }
        )

        // 添加本地流
        peerConnection!!.addTrack(localVideoTrack)
        peerConnection!!.addTrack(localAudioTrack)
    }

    // 创建Offer
    fun createOffer() {
        createPeerConnection()

        val constraints = MediaConstraints().apply {
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
        }

        peerConnection!!.createOffer(object : SdpObserver {
            override fun onCreateSuccess(sdp: SessionDescription) {
                peerConnection!!.setLocalDescription(object : SdpObserver {
                    override fun onSetSuccess() {
                        signalingClient.sendOffer(sdp.description)
                    }

                    override fun onSetFailure(error: String) {
                        Log.e("WebRTC", "Set local description failed: $error")
                    }

                    override fun onCreateSuccess(p0: SessionDescription?) {}
                    override fun onCreateFailure(p0: String?) {}
                }, sdp)
            }

            override fun onCreateFailure(error: String) {
                Log.e("WebRTC", "Create offer failed: $error")
            }

            override fun onSetSuccess() {}
            override fun onSetFailure(p0: String?) {}
        }, constraints)
    }

    // 信令回调
    override fun onUserJoined(userId: String) {
        Log.d("WebRTC", "User joined: $userId")
        createOffer()
    }

    override fun onOfferReceived(userId: String, sdp: String) {
        createPeerConnection()

        val remoteSdp = SessionDescription(SessionDescription.Type.OFFER, sdp)
        peerConnection!!.setRemoteDescription(object : SdpObserver {
            override fun onSetSuccess() {
                peerConnection!!.createAnswer(object : SdpObserver {
                    override fun onCreateSuccess(sdp: SessionDescription) {
                        peerConnection!!.setLocalDescription(object : SdpObserver {
                            override fun onSetSuccess() {
                                signalingClient.sendAnswer(sdp.description)
                            }

                            override fun onSetFailure(error: String) {}
                            override fun onCreateSuccess(p0: SessionDescription?) {}
                            override fun onCreateFailure(p0: String?) {}
                        }, sdp)
                    }

                    override fun onCreateFailure(error: String) {}
                    override fun onSetSuccess() {}
                    override fun onSetFailure(p0: String?) {}
                }, MediaConstraints())
            }

            override fun onSetFailure(error: String) {}
            override fun onCreateSuccess(p0: SessionDescription?) {}
            override fun onCreateFailure(p0: String?) {}
        }, remoteSdp)
    }

    override fun onAnswerReceived(userId: String, sdp: String) {
        val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, sdp)
        peerConnection!!.setRemoteDescription(object : SdpObserver {
            override fun onSetSuccess() {
                Log.d("WebRTC", "Remote description set successfully")
            }

            override fun onSetFailure(error: String) {}
            override fun onCreateSuccess(p0: SessionDescription?) {}
            override fun onCreateFailure(p0: String?) {}
        }, remoteSdp)
    }

    override fun onIceCandidateReceived(userId: String, sdpMid: String, sdpMLineIndex: Int, candidate: String) {
        val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidate)
        peerConnection!!.addIceCandidate(iceCandidate)
    }

    override fun onUserLeft(userId: String) {
        peerConnection?.close()
        peerConnection = null
    }

    // 释放资源
    fun release() {
        localVideoTrack?.dispose()
        localAudioTrack?.dispose()
        peerConnection?.close()
        peerConnectionFactory?.dispose()
        signalingClient.disconnect()
    }
}

7. 5个行业应用案例

案例1:智慧屏远程预览

业务场景:

某智能硬件厂商的智慧屏产品,需要支持手机APP远程查看屏幕内容。

技术方案:

架构设计:

手机APP ←─ P2P ─→ 智慧屏设备
     ↓           ↓
  信令服务器   STUN/TURN服务器

WebRTC配置:

// 智慧屏端:屏幕采集
val screenCapturer = ScreenCapturerAndroid(
    mediaProjectionPermissionResultData,
    object : MediaProjection.Callback() {
        override fun onStop() {
            Log.d("WebRTC", "Screen capture stopped")
        }
    }
)

val videoSource = peerConnectionFactory.createVideoSource(true)  // isScreencast=true
screenCapturer.initialize(
    SurfaceTextureHelper.create("ScreenCapture", eglBase.eglBaseContext),
    context,
    videoSource.capturerObserver
)

// 1080p 30fps 屏幕采集
screenCapturer.startCapture(1920, 1080, 30)

val videoTrack = peerConnectionFactory.createVideoTrack("screen1", videoSource)

实施效果:

指标RTMP推流方案WebRTC P2P提升
延迟3-5秒<500ms90%
带宽成本$0.15/GB$0(P2P)100%
连接成功率99%85%(NAT穿透)-14%
服务器成本$500/月$50/月(仅信令)90%

案例2:视频会议系统

业务场景:

某企业的视频会议系统,支持10人同时在线。

技术方案:

Mesh vs SFU:

Mesh模式(P2P Mesh):
  每个参与者与其他所有人建立P2P连接

  10人会议:
  连接数 = 10 × 9 / 2 = 45条连接
  每人上行带宽 = 9 × 1Mbps = 9Mbps

  缺点:上行带宽压力大

SFU模式(Selective Forwarding Unit):
  所有参与者连接到SFU服务器
  SFU负责转发

  10人会议:
  连接数 = 10条连接
  每人上行带宽 = 1 × 1Mbps = 1Mbps

  优点:节省客户端带宽

SFU服务器(Janus Gateway):

# 部署Janus Gateway
docker run -d \
  --name janus \
  -p 8088:8088 \
  -p 8188:8188 \
  -p 8889:8889 \
  -p 10000-10200:10000-10200/udp \
  canyan/janus-gateway

实施效果:

指标Mesh模式SFU模式提升
最大参会人数4人50人+1150%
客户端上行带宽9Mbps(10人)1Mbps89%
CPU占用80%(10人)30%62%

案例3:远程教育直播

业务场景:

某在线教育平台,支持老师直播授课,学生实时观看和互动。

技术方案:

超低延迟直播:

老师端(WebRTC推流)
    ↓ P2P或中继
SFU服务器
    ↓ WebRTC分发
学生端(WebRTC播放)

延迟:<1秒(vs RTMP/HLS 5-30秒)

实施效果:

指标RTMP直播WebRTC直播提升
延迟5-10秒<1秒90%
互动体验差(延迟高)好(实时)-
CDN成本$1000/月$200/月(SFU)80%

8. 性能优化实战

8.1 码率优化

// 动态码率调整
class DynamicBitrateOptimizer {
    fun optimize(stats: RTCStatsReport) {
        val videoStats = stats.statsMap.values
            .filterIsInstance<RTCOutboundRTPStreamStats>()
            .firstOrNull { it.mediaType == "video" }

        if (videoStats != null) {
            val packetsSent = videoStats.packetsSent
            val packetsLost = videoStats.packetsLost ?: 0
            val lossRate = packetsLost.toFloat() / packetsSent

            when {
                lossRate < 0.02 -> increaseQuality()
                lossRate > 0.10 -> decreaseQuality()
            }
        }
    }

    private fun increaseQuality() {
        // 提升分辨率或码率
    }

    private fun decreaseQuality() {
        // 降低分辨率或码率
    }
}

8.2 网络自适应

// 弱网优化
fun optimizeForPoorNetwork() {
    // 1. 降低分辨率
    videoCapturer.changeCaptureFormat(640, 480, 15)

    // 2. 降低码率
    bitrateController.applyBitrate(300)  // 300kbps

    // 3. 启用FEC(Forward Error Correction)
    val params = peerConnection.senders[0].parameters
    params.encodings[0].apply {
        maxBitrateBps = 300 * 1000
        minBitrateBps = 100 * 1000
        numTemporalLayers = 1  // 单层编码
    }
    peerConnection.senders[0].parameters = params
}

8.3 CPU优化

// 硬件编解码
val videoEncoderFactory = DefaultVideoEncoderFactory(
    eglBase.eglBaseContext,
    true,  // enableIntelVp8Encoder(使用硬件VP8编码器)
    true   // enableH264HighProfile(使用硬件H.264编码器)
)

// 性能对比:
// 软件编码:CPU占用 60-80%
// 硬件编码:CPU占用 10-20%

9. 故障排查与监控

问题1:P2P连接失败

现象:
ICE连接状态一直是"checking",无法建立P2P连接。

原因:

  1. 双方都是Symmetric NAT
  2. TURN服务器未配置
  3. 防火墙阻止UDP

排查:

peerConnection.stats.then { stats ->
    val iceStats = stats.statsMap.values
        .filterIsInstance<RTCIceCandidatePairStats>()

    iceStats.forEach { pair ->
        Log.d("WebRTC", """
            Local: ${pair.localCandidateId}
            Remote: ${pair.remoteCandidateId}
            State: ${pair.state}
            Bytes Sent: ${pair.bytesSent}
            Bytes Received: ${pair.bytesReceived}
        """.trimIndent())
    }
}

解决:

  1. 配置TURN服务器(中继)
  2. 检查防火墙规则
  3. 使用TCP候选者(备用)

问题2:音视频不同步

现象:
视频比音频快2-3秒。

原因:

  1. 音视频时间戳不同步
  2. Jitter Buffer配置不当

解决:

// 同步音视频时间戳
val videoTrack = stream.videoTracks[0]
val audioTrack = stream.audioTracks[0]

videoTrack.setEnabled(false)
audioTrack.setEnabled(false)

// 重新启动,确保时间戳对齐
Handler().postDelayed({
    videoTrack.setEnabled(true)
    audioTrack.setEnabled(true)
}, 100)

10. P2P vs 中继对比

维度P2P直连TURN中继RTMP推流
延迟<100ms<200ms3-5秒
带宽成本0(P2P)中(中继流量)高(CDN流量)
成功率70-90%100%100%
服务器要求信令服务器TURN服务器流媒体服务器
可扩展性低(点对点)中高(CDN)

总结

3句话记住P2P视频传输

  1. WebRTC是标准:浏览器原生支持,NAT穿透自动化,端到端加密
  2. STUN/TURN是核心:STUN获取公网地址,TURN在P2P失败时中继数据
  3. 低延迟是优势:<100ms延迟,适合实时互动场景

核心要点

技术选型:

  • 实时视频通话:WebRTC P2P
  • 多人会议:WebRTC SFU
  • 大规模直播:RTMP/HLS CDN

关键技术:

  • ICE:NAT穿透框架
  • STUN:获取公网地址
  • TURN:P2P失败时中继
  • SDP:会话描述协议

最佳实践:

  • 优先P2P,失败降级TURN
  • 使用SFU支持多人会议
  • 动态调整码率适应网络
  • 监控统计数据优化体验
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐