前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebRTC实现p2p视频通话

WebRTC实现p2p视频通话

作者头像
random_wang
发布2019-10-22 14:43:02
6.6K0
发布2019-10-22 14:43:02
举报
文章被收录于专栏:randomrandom

简介

目的 帮助自己了解webrtc 实现端对端通信

代码语言:javascript
复制
  # 使用流程
  git clone https://gitee.com/wjj0720/webrtc.git
  cd ./webRTC
  npm i
  npm run dev

  # 访问 127.0.0.1:3003/test-1.html 演示h5媒体流捕获
  # 访问 127.0.0.1:3003/local.html 演示rtc 本地传输
  # 访问 127.0.0.1:3003/p2p.html 演示局域网端对端视屏    

what is WebRTC

代码语言:javascript
复制
  WebRTC(Web Real-Time Communication) 网页即时通信 ,是一个支持网页浏览器进行实时语音、视频对话的API。
  于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准
代码语言:javascript
复制
  闲话:目前主流实时流媒体 实现方式
  RTP :(Real-time Transport Protocol) 建立在 UDP 协议上的一种协议加控制

  HLS(HTTP Live Streamin)苹果公司实现的基于HTTP的流媒体传输协议

  RTMP(Real Time Messaging Protocol) Adobe公司基于TCP

  WebRTC google 基于RTP协议

WebRTC组成

zucheng.webp.jpg
zucheng.webp.jpg
  • getUserMedia负责获取用户本地的多媒体数据
  • RTCPeerConnection负责建立P2P连接以及传输多媒体数据。
  • RTCDataChannel提供的一个信令通道实现双向通信

h5 获取媒体流

目标:打开摄像头将媒体流显示到页面

MediaDevices 文档

代码语言:javascript
复制
  navigator.mediaDevices.getUserMedia({
    video: true, // 摄像头
    audio: true // 麦克风
  }).then(steam => {
    // video标签的srcObject
    video.srcObject = stream
  }).catch(e => {
    console.log(e)
  })

RTCPeerConnection

RTCPeerConnection api提供了 WebRTC端创建、链接、保持、监控闭连接的方法的实现 RTCPeerConnection MDN

  1. webRTC流程
peer2peertimeline.png
peer2peertimeline.png
代码语言:javascript
复制
  以 A<=>B 创建p2p连接为例
  
  A端:
    1.创建RTCPeerConnection实例:peerA
    2.将自己本地媒体流(音、视频)加入实例,peerA.addStream
    3.监听来自远端传输过来的媒体流 peerA.onaddstream
    4.创建[SDP offer]目的是启动到远程(此时的远端也叫候选人)))对等点的新WebRTC连接 peerA.createOffer 
    5.通过[信令服务器]将offer传递给呼叫方
    6.收到answer后去[stun]服务拿到自己的IP,通过信令服务将其发送给呼叫放

  B端:
    1.收到信令服务的通知 创建RTCPeerConnection peerB,
    2.也需要将自己本地媒体流加入通信 peerB.addstream
    3.监听来自远端传输过来的媒体流 peerA.onaddstream
    4.同样创建[SDP offer] peerA.createAnswer
    5.通过[信令服务器]将Answer传递给呼叫方
    6.收到对方IP 同样去[stun]服务拿到自己的IP 传递给对方

    至此完成p2p连接 触发双发onaddstream事件
  1. 信令服务 信令服务器: webRTC中负责呼叫建立、监控(Supervision)、拆除(Teardown)的系统 为什么需要: webRTC是p2p连接,那么连接之前如何获得对方信息,有如何将自己的信息发送给对方,这就需要信令服务
  2. SDP 什么是SDP SDP 完全是一种会话描述格式 ― 它不属于传输协议 它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP) SDP协议是基于文本的协议,可扩展性比较强,这样就使其具有广泛的应用范围。 WebRTC中SDP SDP不支持会话内容或媒体编码的协商。webrtc中sdp用于媒体信息(编码解码信息)的描述,媒体协商这一块要用RTP来实现
  3. stun 1.什么是STUN STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。这种通过穿过路由直接通信的方式叫穿墙 2.什么是NAT NAT(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

  1. 前端 <!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 = host this.socketPath = socketPath this.socket = null this.calledHandle = calledHandle this.getCallReject = getCallReject this.peer = null this.localMedia = null } async init() { this.socket = await this.connentSocket() return this } async connentSocket() { if (this.socket) return this.socket return new Promise((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 e reject() }) // 呼叫被接受 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) return this.socket.on(name, (data) => { cb.call(this, data) }) } sendMessage(name, data) { if (!this.socket) return this.socket.emit(name, data) } // 获取本地媒体流 async getLocalMedia() { let localMedia = await navigator.mediaDevices .getUserMedia({ video: { facingMode: "user" }, audio: true }) .catch(e => { console.log(e) }) this.localMedia = localMedia return this } // 设置媒体流到video setMediaTo(eleId, media) { document.getElementById(eleId).srcObject = media } // 被叫响应 called(callingInfo) { this.calledHandle && this.calledHandle(callingInfo) } // 创建RTC createLoacalPeer() { 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 offer async createOffer(cb) { if (!this.peer) return let 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) return this.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 } // 关闭RTC closeRTC() { // .... } } </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>
  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) { 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) } }, findName(id) { return this.list.find(u => u.id === id).name } } socketIO.on('connection', function(socket) { console.log('连接加入.', socket.id) socket.on('addUser', function(data) { console.log(data.name, '加入房间') let user = { id: socket.id, name: data.name, calling: false } userRoom.add(user).sendAllUser('updateUserList', userRoom.list) }) socket.on('sendMessage', ({ content }) => { console.log('转发消息:', content) userRoom.sendAllUser('updateMessageList', { userId: socket.id, content, user: userRoom.findName(socket.id) }) }) socket.on('iceCandidate', ({ id, iceCandidate }) => { console.log('转发信道') userRoom.sendTo(id)('iceCandidate', { iceCandidate, id: socket.id }) }) socket.on('offer', ({id, offer}) => { console.log('转发offer') userRoom.sendTo(id)('called', { offer, id: socket.id, name: userRoom.findName(socket.id)}) }) socket.on('answer', ({id, answer}) => { console.log('接受视频'); userRoom.sendTo(id)('answer', {answer}) }) socket.on('rejectCall', id => { console.log('转发拒接视频') userRoom.sendTo(id)('callRejected') }) socket.on('disconnect', () => { // 断开删除 console.log('连接断开', socket.id) userRoom.del(socket.id).sendAllUser('updateUserList', userRoom.list) }) }) module.exports = socketIO // www.js 这就不关键了 const http = require('http') const app = require('../app') const socketIO = require('../socket.js') const server = http.createServer(app.callback()) socketIO.attach(server) server.listen(3003, () => { console.log('server start on 127.0.0.1:3003') })

搭建 STUN/TURN

因为没有钱买服务器 没试过

coturn 据说使用它搭建 STUN/TURN 服务非常的方便

代码语言:javascript
复制
  # 编译
  cd coturn
  ./configure --prefix=/usr/local/coturn
  sudo make -j 4 && make install

  # 配置
  listening-port=3478        #指定侦听的端口
  external-ip=39.105.185.198 #指定云主机的公网IP地址
  user=aaaaaa:bbbbbb         #访问 stun/turn服务的用户名和密码
  realm=stun.xxx.cn          #域名,这个一定要设置
 
  #启动
  cd /usr/local/coturn/bin
  turnserver -c ../etc/turnserver.conf

  trickle-ice https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice 按里面的要求输入 stun/turn 地址、用户和密码 
  输入的信息分别是: 
    STUN or TURN URI 的值为: turn:stun.xxx.cn
    用户名为: aaaaaa
    密码为: bbbbbb

STUN参数传递

代码语言:javascript
复制
  let ice = {"iceServers": [
    {"url": "stun:stun.l.google.com:19302"},  // 无需密码的
    // TURN 一般需要自己去定义
    {
      'url': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', // 密码
      'username': '28224511:1379330808' // 用户名
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]}
  // 可以提供多iceServers地址,但RTC追选择一个进行协商


  // 实例化的是给上参数 RTC会在合适的时候去获取本地墙后IP
  let pc = new RTCPeerConnection(ice);

  /*
    // 据说这些免费的地址都可以用
    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:23.21.150.121
    stun:stun01.sipphone.com
    stun:stun.ekiga.net
    stun:stun.fwdnet.net
    stun:stun.ideasip.com
    stun:stun.iptel.org
    stun:stun.rixtelecom.se
    stun:stun.schlund.de
    stun:stunserver.org
    stun:stun.softjoys.com
    stun:stun.voiparound.com
    stun:stun.voipbuster.com
    stun:stun.voipstunt.com
    stun:stun.voxgratia.org
    stun:stun.xten.com
  */
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • what is WebRTC
  • WebRTC组成
  • h5 获取媒体流
  • RTCPeerConnection
  • SHOW THE CODE
  • 搭建 STUN/TURN
  • STUN参数传递
相关产品与服务
NAT 网关
NAT 网关(NAT Gateway)提供 IP 地址转换服务,为腾讯云内资源提供高性能的 Internet 访问服务。通过 NAT 网关,在腾讯云上的资源可以更安全的访问 Internet,保护私有网络信息不直接暴露公网;您也可以通过 NAT 网关实现海量的公网访问,最大支持1000万以上的并发连接数;NAT 网关还支持 IP 级流量管控,可实时查看流量数据,帮助您快速定位异常流量,排查网络故障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档