流媒体协议之 WebRTC 实现 p2p 视频通话(二)
WebRTC google 基于 RTP 协议
WebRTC 组成
getUserMedia 负责获取用户本地的多媒体数据
RTCPeerConnection 负责建立 P2P 连接以及传输多媒体数据。
RTCDataChannel 提供的一个信令通道实现双向通信
h5 获取媒体流
目标:打开摄像头将媒体流显示到页面
navigator.mediaDevices.getUserMedia({video: true, // 摄像头 audio: true // 麦克风}).then(steam => {// video 标签的 srcObjectvideo.srcObject = stream}).catch(e => {console.log(e)})
RTCPeerConnection
RTCPeerConnection api 提供了 WebRTC 端创建、链接、保持、监控闭连接的方法的实现RTCPeerConnection MDN
webRTC 流程
以 A<=>B 创建 p2p 连接为例
A 端:1.创建 RTCPeerConnection 实例:peerA2.将自己本地媒体流(音、视频)加入实例,peerA.addStream3.监听来自远端传输过来的媒体流 peerA.onaddstream4.创建[SDP offer]目的是启动到远程(此时的远端也叫候选人)))对等点的新 WebRTC 连接 peerA.createOffer5.通过[信令服务器]将 offer 传递给呼叫方 6.收到 answer 后去[stun]服务拿到自己的 IP,通过信令服务将其发送给呼叫放
B 端:1.收到信令服务的通知 创建 RTCPeerConnection peerB,2.也需要将自己本地媒体流加入通信 peerB.addstream3.监听来自远端传输过来的媒体流 peerA.onaddstream4.同样创建[SDP offer] peerA.createAnswer5.通过[信令服务器]将 Answer 传递给呼叫方 6.收到对方 IP 同样去[stun]服务拿到自己的 IP 传递给对方
至此完成 p2p 连接 触发双发 onaddstream 事件。1.信令服务信令服务器:webRTC 中负责呼叫建立、监控(Supervision)、拆除(Teardown)的系统为什么需要:webRTC 是 p2p 连接,那么连接之前如何获得对方信息,有如何将自己的信息发送给对方,这就需要信令服务。2.SDP
什么是 SDPSDP 完全是一种会话描述格式 ― 它不属于传输协议它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)SDP 协议是基于文本的协议,可扩展性比较强,这样就使其具有广泛的应用范围。
WebRTC 中 SDPSDP 不支持会话内容或媒体编码的协商。webrtc 中 sdp 用于媒体信息(编码解码信息)的描述,媒体协商这一块要用 RTP 来实现。3.STUN1.什么是 STUNSTUN(Session Traversal Utilities for NAT,NAT 会话穿越应用程序)是一种网络协议,它允许位于 NAT(或多重 NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的 Internet 端端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间创建 UDP 通信。这种通过穿过路由直接通信的方式叫穿墙
2.什么是 NATNAT(Network Address Translation,网络地址转换),是 1994 年提出的。当在专用网内部的一些主机本来已经分配到了本地 IP 地址,但现在又想和因特网上的主机通信时,于是乎在路由器上安装 NAT 软件。装有 NAT 软件的路由器叫做 NAT 路由器,它可以通过一个全球 IP 地址。使所有使用本地地址的主机在和外界通信时,这种通过使用少量的公有 IP 地址代表较多的私有 IP 地址的方式,将有助于减缓可用的 IP 地址空间的枯竭
3.WebRTC 的穿墙目前常用的针对 UDP 连接的 NAT 穿透方法主要有:STUN、TURN、ICE、uPnP 等。其中 ICE 方式由于其结合了 STUN 和 TURN 的特点 webrtc 是用的就是这个 google 提供的免费地址:https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
SHOW THE CODE
前端
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta http-equiv="X-UA-Compatible" content="ie=edge" /><title>端对端</title></head><body><div class="page-container"><div class="message-box"><ul class="message-list"></ul><div class="send-box"><textarea class="send-content"></textarea><button class="sendbtn">发送</button></div></div><div class="user-box"><video id="local-video" autoplay class="local-video"></video><video id="remote-video" autoplay class="remote-video"></video><p class="title">在线用户</p><ul class="user-list"></ul></div><div class="mask"><div class="mask-content"><input class="myname" type="text" placeholder="输入用户名加入房间"><button class="add-room">加入</button></div></div><div class="video-box">
</div></div><script src="/js/jquery.js"></script><script src="/js/socket.io.js"></script><script>// 简单封装一下 class Chat {constructor({ calledHandle, host, socketPath, getCallReject } = {}) {this.host = hostthis.socketPath = socketPaththis.socket = nullthis.calledHandle = calledHandlethis.getCallReject = getCallRejectthis.peer = nullthis.localMedia = null}async init() {this.socket = await this.connentSocket()return this}async connentSocket() {if (this.socket) return this.socketreturn new Pro
mise((resolve, reject) => {let socket = io(this.host, { path: this.socketPath })socket.on("connect", () => {console.log("连接成功!")resolve(socket)})socket.on("connect_error", e => {console.log("连接失败!")throw ereject()})// 呼叫被接受 socket.on('answer', ({ answer }) => {this.peer && this.peer.setRemoteDescription(answer)})// 被呼叫事件 socket.on('called', callingInfo => {this.called && this.called(callingInfo)})// 呼叫被拒 socket.on('callRejected', () => {this.getCallReject && this.getCallReject()})
socket.on('iceCandidate', ({ iceCandidate }) => {console.log('远端添加 iceCandidate');this.peer && this.peer.addIceCandidate(new RTCIceCandidate(iceCandidate))})
})}addEvent(name, cb) {if (!this.socket) returnthis.socket.on(name, (data) => {cb.call(this, data)})}sendMessage(name, data) {if (!this.socket) returnthis.socket.emit(name, data)}// 获取本地媒体流 async getLocalMedia() {let localMedia = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: true }).catch(e => {console.log(e)})this.localMedia = localMediareturn this}// 设置媒体流到 videosetMediaTo(eleId, media) {document.getElementById(eleId).srcObject = media}// 被叫响应 called(callingInfo) {this.calledHandle && this.calledHandle(callingInfo)}// 创建 RTCcreateLoacalPeer() {this.peer = new RTCPeerConnection()return this}// 将媒体流加入通信 addTrack() {if (!this.peer || !this.localMedia) return//this.localMedia.getTracks().forEach(track => this.peer.addTrack(track, this.localMedia));this.peer.addStream(this.localMedia)return this}// 创建 SDP offerasync createOffer(cb) {if (!this.peer) returnlet offer = await this.peer.createOffer({ OfferToReceiveAudio: true, OfferToReceiveVideo: true })this.peer.setLocalDescription(offer)cb && cb(offer)return this}async createAnswer(offer, cb) {if (!this.peer) returnthis.peer.setRemoteDescription(offer)let answer = await this.peer.createAnswer({ OfferToReceiveAudio: true, OfferToReceiveVideo: true })this.peer.setLocalDescription(answer)cb && cb(answer)return this
}listenerAddStream(cb) {this.peer.addEventListener('addstream', event => {console.log('addstream 事件触发', event.stream);cb && cb(event.stream);})return this}// 监听候选加入 listenerCandidateAdd(cb) {this.peer.addEventListener('icecandidate', event => {let iceCandidate = event.candidate;if (iceCandidate) {console.log('发送 candidate 给远端');cb && cb(iceCandidate);}
})return this}// 检测 ice 协商过程 listenerGatheringstatechange () {this.peer.addEventListener('icegatheringstatechange', e => {console.log('ice 协商中: ', e.target.iceGatheringState);})return this}// 关闭 RTCcloseRTC() {// ....}}</script><script>$(function () {
let chat = new Chat({host: 'http://127.0.0.1:3003',socketPath: "/websocket",calledHandle: calledHandle,getCallReject: getCallReject})
// 更新用户列表视图 function updateUserList(list) {$(".user-list").html(list.reduce((temp, li) => {temp += <li class="user-li">${li.name} <button data-calling=${li.calling} data-id=${li.id} class=${li.id === this.socket.id || li.calling ? 'cannot-call' : 'can-call'}> 通话</button></li>
return temp}, ''))}// 更新消息 li 表视图 function updateMessageList(msg) {$('.message-list').append(<li class=${msg.userId === this.socket.id ? 'left' : 'right'}>${msg.user}: ${msg.content}</li>
)}
// 加入房间('.add-room').on('click', async () => {let name = ('.myname').val()if (!name) return$('.mask').fadeOut()await chat.init()// 用户加入事件 chat.addEvent('updateUserList', updateUserList)// 消息更新事件 chat.addEvent('updateMessageList', updateMessageList)
chat.sendMessage('addUser', { name })
})// 发送消息('.sendbtn').on('click', () => {let sendContent = ('.send-content').val()if (!sendContent) return$('.send-content').val('')chat.sendMessage('sendMessage', { content: sendContent })})
// 视屏('.user-list').on('click', '.can-call', async function () {// 被叫方信息let calledParty = (this).data()if (calledParty.calling) return console.log('对方正在通话');
// 初始本地视频 $('.local-video').fadeIn()await chat.getLocalMedia()chat.setMediaTo('local-video', chat.localMedia)
chat.createLoacalPeer().listenerGatheringstatechange().addTrack().listenerAddStream(function (stream) {
$('.remote-video').fadeIn()chat.setMediaTo('remote-video', stream)
}).listenerCandidateAdd(function (iceCandidate) {
chat.sendMessage('iceCandidate', { iceCandidate, id: calledParty.id })
}).createOffer(function (offer) {
chat.sendMessage('offer', { offer, ...calledParty })
})})
//呼叫被拒绝 function getCallReject() {chat.closeRTC()$('.local-video').fadeIn()console.log('呼叫被拒');}
// 被叫 async function calledHandle(callingInfo) {if (!confirm(是否接受${callingInfo.name}的视频通话
)) {chat.sendMessage('rejectCall', callingInfo.id)return}
$('.local-video').fadeIn()await chat.getLocalMedia()chat.setMediaTo('local-video', chat.localMedia)
chat.createLoacalPeer().listenerGatheringstatechange().addTrack().listenerCandidateAdd(function (iceCandidate) {
chat.sendMessage('iceCandidate', { iceCandidate, id: callingInfo.id })
}).listenerAddStream(function (stream) {
$('.remote-video').fadeIn()chat.setMediaTo('remote-video', stream)
}).createAnswer(callingInfo.offer, function (answer) {
chat.sendMessage('answer', { answer, id: callingInfo.id })
})
}
})</script></body></html>后端
const SocketIO = require('socket.io')const socketIO = new SocketIO({path: '/websocket'})
let userRoom = {list: [],add(user) {this.list.push(user)return this},del(id) {this.list = this.list.filter(u => u.id !== id)return this},sendAllUser(name, data) {this.list.forEach(({ id }) => {console.log('>>>>>', id)socketIO.to(id).emit(name, data)})return this},sendTo(id) {return (eventName, data) => {socketIO.to(id).emit(eventName, data)}
2.后端
const SocketIO = require('socket.io')const socketIO = new SocketIO({path: '/websocket'})
let userRoom = {list: [],add(user) {this.list.push(user)return this},del(id) {this.list = this.list.filter(u => u.id !== id)return this},sendAllUser(name, data) {
评论