WebRTC
花了两天时间简单了解了一下WEB RTC,并由此写入三个DEMO。
- p2p 点对点
- o2m 一对多
- live 直播
目前主要都是按p2p进行的简单扩展。
WebRTC 简单了解
目前资料不算少,不过确实也不多,而且理论偏多,新手入门其实还是有点压力的。
这边推荐几个资料和视频。
MDN 文档 记得出问题看看文档先
WebRTC samples 没有思路的时候记得看看
哔哩哔哩 - 一只斌 这个b站up,大概算是由浅入深讲了这个东西,但是有些基础概念被带过了(应该主要是我基础较薄弱),其中圣诞特辑中的流程其实还是比较清晰的。
知乎 - 为什么webrtc那么贵? 注意看评论区
知乎 - 可以用WebRTC来做视频直播吗? 注意看评论区
WebRTC 的一些概念
WebRTC基础情况下只需要一个 ‘信令服务’ 作为业务需求,并不需要直接管理流。
p2p
点对点通信(pc与pc直接通信),不通过服务器
信令服务
用于建立通信和业务交互的服务端
SDP
存放媒体信息、会话的描述。如编码解码信息
NAT / STUN / TURN / ice
strn
用于p2p连接。(下面仅个人理解
由于没有公网ip的两个主机没有办法直接进行直接通信,所以需要一个“中转的服务器”,但是由于中转服务器过于依赖服务器带宽,所以采用NAT穿刺,这样双方通信就不需要依赖服务器。
ice
整合了STUN和TURN的框架
实际具体不需要管,ice 服务器可以使用公开的
实践
webrtc建立还是很简单的,只需要交换双方sdp和ice-candidate,即可建立通信。
具体流程
p2p 1对1 视频
呼叫方创建
offer sdp接收方根据offer sdp创建answer sdp
一、 sdp 交换
- 呼叫方 建立WebRTC
- 接收方 等待 信令服务器 转发 类型为
offer的sdp - 呼叫方 监听
onnegotiationneeded并创建offer sdp并调用setLocalDescription设置为本地描述 - 呼叫方 向 信令服务器 发送
offer sdp并监听answer sdp - 接收方 得到
offer sdp并调用setRemoteDescription设置为远程描述 - 接收方 创建
answer sdp并设置本地描述(setLocalDescription) 同时向 信令服务器 发送answer sdp - 呼叫方 收到
answer sdp并设远程描述(setRemoteDescription)
二、 ice-candidate 交换
- 监听
onicecandidate得到candidate后进行发送 - 监听
信令服务器ice-candidate 得到后调用addIceCandidate
获取媒体 流
1function getUserMedia(constrains) { 2 let promise = null; 3 if (navigator.mediaDevices.getUserMedia) { 4 //最新标准API 5 promise = navigator.mediaDevices.getUserMedia(constrains) 6 } else if (navigator.webkitGetUserMedia) { 7 //webkit内核浏览器 8 promise = navigator.webkitGetUserMedia(constrains) 9 } else if (navigator.mozGetUserMedia) { 10 //Firefox浏览器 11 promise = navagator.mozGetUserMedia(constrains) 12 } else if (navigator.getUserMedia) { 13 //旧版API 14 promise = navigator.getUserMedia(constrains); 15 } 16 return promise; 17} 18// 得到流 19const stream = await getUserMedia({ 20 video: true, 21 audio: true, 22}); 23// 展示 24$('video').srcObject = stream;
信令服务器
demo使用 node + socket-io 做信令服务器
目前逻辑很简单,只为数据定向转发
目前主要对sdp和ice candidate进行一个定向转发
1const SocketIo = require('socket.io'); 2const consola = require('consola'); // log 工具 3 4const users = new Map(); // 用户存储 5 6/** 7 * 8 * @param {http.Server} server 9 */ 10const signaling = (server) => { 11 // 创建socketio服务 12 const io = new SocketIo.Server(server); 13 14 const p2p = io.of('/p2p'); 15 // 连接 16 p2p.on('connect', (socket) => { 17 consola.info('[%s] connect', socket.id); 18 19 // ********** 20 // 用户操作 21 22 // sdp 转发 23 socket.on('sdp', (data) => { 24 console.log('sdp data.to[%s] type[%s]', data.to, data.type); 25 const user = users.get(data.to) 26 if (user) { 27 user.emit('sdp', data); 28 } 29 }); 30 // ice-candidate 转发 31 socket.on('ice-candidate', (data) => { 32 console.log('ice-candidate data.to', data.to); 33 const user = users.get(data.to) 34 if (user) { 35 user.emit('ice-candidate', data); 36 } 37 }); 38 39 // 用户操作 40 // ********** 41 42 // ---------- 43 // 用户操作 44 socket.once('disconnect', () => { 45 consola.info('[%s] disconnect', socket.id); 46 users.delete(socket.id); 47 p2p.emit('leave', { 48 user: socket.id 49 }); 50 }); 51 socket.emit('users', { 52 users: Array.from(users.keys()) 53 }); 54 p2p.emit('join', { 55 user: socket.id 56 }); 57 users.set(socket.id, socket); 58 // 用户操作 59 // ---------- 60 }); 61 62}; 63 64module.exports = signaling;
建立WebRTC
这里在代码中区分了发送和接收具体可参考业务
呼叫方
1// ************ 2// 呼叫方 3// ************ 4 5// 1. 建立rct连接 6const pc = new RTCPeerConnection({ 7 iceServers: [ 8 { 9 urls: ["stun:stun.counterpath.net:3478"] // 可以直接百度找一些开放的stun服务器 10 } 11 ] 12}); 13// 2. 绑定流 14const stream = await getUserMedia({ 15 video: true, 16 audio: true, 17}); 18// 添加媒体轨道 如果 video 和 audio 都为true 则 getTracks 可以获得两个轨道 19stream.getTracks().forEach(track => pc[toUser].addTrack(track, stream)); 20 21// 3. 监听 22pc.onnegotiationneeded = ()=>{ 23 pc 24 .createOffer() // 创建 offer sdp 25 .then((offer) => { 26 // 设置为本地描述 27 return pc.setLocalDescription(offer); 28 }) 29 .then(() => { 30 // 定向转发 sdp 31 socket.emit('sdp', { 32 type: 'sender', 33 value: pc.localDescription 34 }); 35 }); 36} 37pc.onicecandidate = (ev)=>{ 38 // 转发 ice-candidate 39 socket.emit('ice-candidate', { 40 type: 'sender', 41 value: ev.candidate, 42 }); 43} 44pc.ontrack = (ev)=>{ 45 // 这里可以的得到对方的流 46 let stream = ev.streams[0]; 47} 48 49// 监听 ice 和 sdp 50socket.on('ice-candidate', (data)=>{ 51 if(data.type === 'receive'){ 52 const candidate = new RTCIceCandidate(data.value); 53 pc.addIceCandidate(candidate) 54 } 55}); 56socket.on('sdp', (data)=>{ 57 if(data.type === 'receive'){ 58 const sdp = new RTCSessionDescription(data.value); 59 pc.setRemoteDescription(sdp); 60 } 61});
接收方
1// ************ 2// 接收方 3// ************ 4 5// 建立rct连接 6const pc = new RTCPeerConnection({ 7 iceServers: [ 8 { 9 urls: ["stun:stun.counterpath.net:3478"] // 可以直接百度找一些开放的stun服务器 10 } 11 ] 12}); 13 14socket.on('sdp', async (data)=>{ 15 if(data.type === 'sender'){ 16 const sdp = new RTCSessionDescription(data.value); 17 pc.setRemoteDescription(sdp); 18 // 绑定流 19 const stream = await getUserMedia({ 20 video: true, 21 audio: true, 22 }); 23 // 添加媒体轨道 如果 video 和 audio 都为true 则 getTracks 可以获得两个轨道 24 stream.getTracks().forEach(track => pc[toUser].addTrack(track, stream)); 25 26 // 监听 27 pc.onnegotiationneeded = ()=>{ 28 pc 29 .createAnswer() // 创建 offer sdp 30 .then((answer) => { 31 // 设置为本地描述 32 return pc.setLocalDescription(answer); 33 }) 34 .then(() => { 35 // 定向转发 sdp 36 socket.emit('sdp', { 37 type: 'receive', 38 value: pc.localDescription 39 }); 40 }); 41 } 42 pc.onicecandidate = (ev)=>{ 43 // 转发 ice-candidate 44 socket.emit('ice-candidate', { 45 type: 'receive', 46 value: ev.candidate, 47 }); 48 } 49 pc.ontrack = (ev)=>{ 50 // 这里可以的得到对方的流 51 let stream = ev.streams[0]; 52 } 53 54 } 55}); 56 57// 监听 ice 和 sdp 58socket.on('ice-candidate', (data)=>{ 59 if(data.type === 'sender'){ 60 const candidate = new RTCIceCandidate(data.value); 61 pc.addIceCandidate(candidate) 62 } 63}); 64
