P2P视频传输完全指南:WebRTC从原理到优化
P2P视频传输完全指南:WebRTC从原理到优化
目录
一句话总结
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
优点:
- 直接连接,延迟低
- 节省服务器带宽
- 可扩展性强
典型应用场景:
- 视频通话:微信视频、Zoom、Skype
- 智能硬件:智慧屏远程预览、IP摄像头查看
- 远程桌面:TeamViewer、向日葵
- 文件传输:P2P文件共享、AirDrop
- 游戏对战:P2P联机游戏
1.2 为什么需要WebRTC?
WebRTC(Web Real-Time Communication) 是Google开源的实时通信框架,提供了P2P音视频传输的完整解决方案。
核心特点:
| 特点 | 说明 | 优势 |
|---|---|---|
| NAT穿透 | 自动STUN/TURN | 90%成功率 |
| 加密传输 | DTLS-SRTP | 端到端加密 |
| 自适应码率 | 根据网络调整 | 流畅播放 |
| 跨平台 | Web/Android/iOS | 统一API |
| 开源免费 | Apache 2.0 | 零成本 |
WebRTC vs 传统流媒体:
| 维度 | WebRTC(P2P) | RTMP/HLS(C/S) |
|---|---|---|
| 延迟 | <100ms | 1-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 |
| Relay | TURN中继地址 | 低 | 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 信令协议设计
信令消息类型:
| 消息类型 | 说明 | 参数 |
|---|---|---|
| offer | Offer SDP | sdp |
| answer | Answer SDP | sdp |
| ice-candidate | ICE候选者 | 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 视频编解码
支持的编解码器:
| 编解码器 | 说明 | 码率 | 延迟 | 质量 |
|---|---|---|---|---|
| VP8 | Google开源 | 中 | 低 | 中 |
| VP9 | VP8升级版 | 低 | 低 | 高 |
| 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秒 | <500ms | 90% |
| 带宽成本 | $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人) | 1Mbps | 89% |
| 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连接。
原因:
- 双方都是Symmetric NAT
- TURN服务器未配置
- 防火墙阻止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())
}
}
解决:
- 配置TURN服务器(中继)
- 检查防火墙规则
- 使用TCP候选者(备用)
问题2:音视频不同步
现象:
视频比音频快2-3秒。
原因:
- 音视频时间戳不同步
- 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 | <200ms | 3-5秒 |
| 带宽成本 | 0(P2P) | 中(中继流量) | 高(CDN流量) |
| 成功率 | 70-90% | 100% | 100% |
| 服务器要求 | 信令服务器 | TURN服务器 | 流媒体服务器 |
| 可扩展性 | 低(点对点) | 中 | 高(CDN) |
总结
3句话记住P2P视频传输
- WebRTC是标准:浏览器原生支持,NAT穿透自动化,端到端加密
- STUN/TURN是核心:STUN获取公网地址,TURN在P2P失败时中继数据
- 低延迟是优势:<100ms延迟,适合实时互动场景
核心要点
技术选型:
- 实时视频通话:WebRTC P2P
- 多人会议:WebRTC SFU
- 大规模直播:RTMP/HLS CDN
关键技术:
- ICE:NAT穿透框架
- STUN:获取公网地址
- TURN:P2P失败时中继
- SDP:会话描述协议
最佳实践:
- 优先P2P,失败降级TURN
- 使用SFU支持多人会议
- 动态调整码率适应网络
- 监控统计数据优化体验
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)