WebRTC入门

WebRTC入门

一、基本原理&流程

1、基本原理

基本实现原理(Peer To Peer)信令与视频通话 - Web API 接口参考

(1)WebRTC服务构成

img

  • 信令服务器:用于连接两个客户端,两个客户端连接时,需要进行协商,相互确定媒体格式,告知在网络中的位置(地址),在本篇使用WebSocket作为信令服务器:
  • WebSocket:一个客户端告知另一个客户端,建立通信请求并携带通讯信息,caller(发送者)发送offer,callee(接受者)接收offer,返回answer,offer/answer包含type(消息类型-offer/answer)、name(发送者用户名)、target(接受者用户名)、SDP(Session Description Protocol会话协议描述,存放视频编码、通讯协议【中间还可能包含身份验证操作】

  • STUN和TURN(提供ICE):

  • STUN:(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。

  • TURN:Traversal Using Relays around NAT,是STUN/RFC5389的一个拓展,主要添加了Relay功能。如果
    终端在NAT之后, 那么在特定的情景下,有可能使得终端无法和其对等端(peer)进行直接的通信,这时就需要公网的服务器作为一个中继, 对来往的数据进行转发。这个转发的协议就被定义为TURN。

(2)Demo流程

[]内为执行主体

参考demo项目地址:https://github.com/chachae/WebRTC-With-Socket

启动本demo需要配置coturn,在第二章可以查看

[被监控客户端(monitor)]启动->
  • [monitor]连接上Websocket

  • [ws]接收monitor用户id,并存入ws服务维护的ConcurrentHashMap(id为key,session为value且该Session为自定义封装,存放有netty-channel,如果该ConcurrentHashMap已存在,不使用新的value覆盖旧的value)

  • [monitor]用Promise异步打开本地音视频,并向ws服务定时不断发送心跳(循环调用/1t1接口,向redis发送广播消息)

    //发送到redis广播中
    subscribe netty-test-channel
    //发送的信息
    Message(fromId=114, toId=114, content=ok, command=heart)
    
  • [ws]receiveMessage接收到redis的广播消息,消息类型为heart,执行handleHeart,状态为正常,关闭当前websocket会话(当超过一段时间没有接受到monitor的心跳,则默认monitor已关闭,此时ws会从ConcurrentHashMap中删除monitor的id)

    但是有可能ws关闭,但客户端并未关闭,后期需要优化

[监控客户端(index)]启动->
  • [index]连接上Websocket

  • [ws]接收index用户id,并存入ws服务器维护的ConcurrentHashMap(此时monitor和index的用户id都存在此ConcurrentHashMap中)

[P2P开始连接阶段]
  • [index]输入任意用户id,点击提交,调用sendMonitor,初始化RTCPeerConnection:

    initPeer() {
      //stun&turn地址配置
      this.myPeerConnection = new RTCPeerConnection(this.configuration);
      添加事件监听函数,接收ICE candidate信息,用于NAT 穿透,本质就是地址信息
      this.myPeerConnection.onicecandidate = this.handleIceCandidate;
      this.myPeerConnection.ontrack = this.handleOnTrack;
      this.RTCPeerConnectionCreated = true;
    },
    

    并将message封装好,调用/1t1接口,传入message,推送到redis广播中

    //index发送给monitor
    {"command":"cmd","fromId":"index","toId":"monitor"}
    
  • [ws]接收redis广播,将toId和ConcurrentHashMap中的对比,没有直接忽略(有则继续),message的command的值为cmd,执行handleCmd,将fromId和toId对换后使用ConcurrentHashMap中根据key(原toId-monitor,现fromId)拿到的Session中Channel发送ws消息给monitor(该消息借助ws发送给toId)

  • [monitor]handleMessage接收到ws通过Websocket(monitor创建时建立的netty-channel)发送来的消息:

    {"command":"cmd","fromId":monitor,"toId":"index"}
    

    然后进入handleCmd,初始化RTCPeerConnection:

    //monitor唯一初识化RTCPeerConnection操作
    initPeer() {
        	//stun&turn地址配置
            this.rtcPeerConnection = new RTCPeerConnection(this.configuration);
            this.rtcPeerConnection.onicecandidate = this.handleIceCandidate;
            for (const track of this.localMediaStream.getTracks()) {
            	this.rtcPeerConnection.addTrack(track, this.localMediaStream);
         }
    }
    

    接下来创建SDP-offer(由monitor调用rtcAPI创建):

    handleCmd(message) {
    	this.cmdUser = message.toId
    	//初始化RTCPeerConnection
    	this.initPeer();
        //创建SDP offer 
    	this.rtcPeerConnection.createOffer(this.offerOptions).then(this.setLocalAndOffer)
    		.catch((e) => {
    			console.log(e)
    		}
    	);
    },
        
    offerOptions格式:
    {
    	offerToReceiveAudio: true, // 告诉另一端,是否想接收音频,默认true
    	offerToReceiveVideo: true, // 告诉另一端,是否想接收视频,默认true
    	iceRestart: false, // 是否在活跃状态重启ICE网络协商
    }
    
    setLocalAndOffer(sessionDescription) {
    	this.rtcPeerConnection.setLocalDescription(sessionDescription);
    	this.sendOne(JSON.stringify({
        command: 'offer',
        fromId: this.loginForm.userId,
        toId: this.cmdUser,
        content: {sdp: sessionDescription}
      }))
    },
    
    此时sessionDescription:
    RTCSessionDescription :
    	{
            type: 'offer', 
            sdp: 'v=0\r\no=- 6155307945155998122 2 IN IP4 127.0.0.1\r\ns…7eJSfKC9gV ee272cf4-2cd4-4a44-9cf9-9d6edf250413\r\n'
        }
    

    最后调用sendOne发送至redis广播

  • [ws]handlerMessage接收monitor发来的SDP-offer消息(from:monitor、to:index),type为offer,调用handleCmd,fromId和toId转换(from:index、to:monitor),拿取toId的channel发送给toId(index)

  • [index]接收到monitor传来的SDP-offer信息:

    Message(fromId=433, toId=756, content={"sdp":{"type":"offer","sdp":"xxxxxxxxx"}}, command=offer)
    

    调用handleMessage接受,type为offer,调用handleOffer,判断RTCConnection标记是否为true,如果为false则实例化,调用rtcAPI接收sdp信息,并创建SDP-answer:

    /***
     * 处理offer
    */
    handleOffer(message) {
    	if (this.RTCPeerConnectionCreated === false) {
    		this.initPeer()
    	}
    	let sdpMessage = message.content;
    	let sdp = JSON.parse(sdpMessage).sdp;
    	//设置连接端的sdp
    	this.myPeerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
        //创建SDP-answer
    	this.myPeerConnection.createAnswer().then(this.setLocalAndAnswer)
            .catch((e) => {
    			console.log(e);
    		}
    	);
    },
              
    /***
     * 处理 offer 并 answer
     */
    setLocalAndAnswer(sessionDescription) {
    	this.myPeerConnection.setLocalDescription(sessionDescription)
    	this.sendOne(JSON.stringify({
    		command: 'answer',
    		fromId: this.loginForm.userId,
    		toId: this.monitorForm.userId,
    		content: {sdp: sessionDescription}
    	}))
    },
    

    index创建好SDP-answer后,发送给ws,ws转换from和to后,转发给monitor

    Message(fromId=756, toId=433, content={"sdp":{"type":"answer","sdp":"xxxx"}}, command=answer)
    
  • [monitor]接收index的SDP-answer,执行handleAnswer,设置远端(index)的SDP信息

    handleAnswer(message) {
    	let sdp = JSON.parse(message.content).sdp;
    	this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(sdp))
    }
    
  • 当index和monitor设置localDescription时,会触发RTCPeerConnection实例.onIcecandidate()时间,rtc获取到RTCPeerConnectionIceEvent,经过ws发送给对方:

    Message(fromId=615, toId=306, content={"candidate":{"sdpMLineIndex":0,"candidate":"candidate:122006300 1 udp 2122260223 192.168.30.1 61672 typ host generation 0 ufrag LLWN network-id 1","sdpMid":"0"}}, command=candidate)
    
  • 双方接收到对方candidate消息后,执行handleCandidate方法:

    handleCandidate(message) {
            let candidate = JSON.parse(message.content).candidate
            this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(candidate)).catch((e) => {
              console.log(e)
    	})
    },
    
  • 因为monitor作为发送视频流方,在创建RTCPeerConnection实例时,将视频流加入到RTCPeerConnection实例中:

    initPeer() {
            //创建RTCPeerConnection
            this.rtcPeerConnection = new RTCPeerConnection(this.configuration);
            this.rtcPeerConnection.onicecandidate = this.handleIceCandidate;
            for (const track of this.localMediaStream.getTracks()) {
              this.rtcPeerConnection.addTrack(track, this.localMediaStream);
    	}
    },
       
    //或者在收到对方Candidate消息时再添加视频流
    handleCandidate(message) {
    	let candidate = JSON.parse(message.content).candidate
    	this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(candidate)).catch((e) => {
    		console.log(e)
    	})
    },
    
  • [index]monitor调用addTrack(track, stream)后,index会接收到monitor的流信息,调用方法后播放:

    this.myPeerConnection.ontrack = this.handleOnTrack;
    
    /***
     * 处理track
     */
    handleOnTrack(event) {
    	let remoteMediaStream = event.streams[0];
    	let remoteVideo = document.getElementById("remoteVideo");
    	remoteVideo.srcObject = remoteMediaStream;
    	remoteVideo.play();
    }
    
  • 监控联通成功(end)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNtbuW8J-1663836322088)(https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling/webrtc_-_signaling_diagram.svg)]

二、STUN+TURN搭建

WebRTC服务搭建之Kurento | 码客 (psvmc.cn)

1、注意开放对应端口,在测试网址上出现Done则成功

2、coturn安装时make可能会报错缺少依赖

​ sudo yum install gcc
​ sudo yum install epel-release

3、无外网环境下只配置内网地址即可

4、其中的stun和turn地址,需要写入上文演示demo中的stun&turn配置中

三、需求场景

可能浏览器会其实要求对应权限

1、电话

//mediaConstraints配置中开启audio 关闭video
navigator.mediaDevices.getUserMedia(this.mediaConstraints)

2、录屏

//screenConstraints配置中可选是否开启audio
navigator.mediaDevices.getDisplayMedia(this.screenConstraints)

3、视频电话

//mediaConstraints配置中开启audio(video默认开启)
navigator.mediaDevices.getUserMedia(this.mediaConstraints)

4、电话+录屏

5、视频通话+录屏

四、需求点

1、通话计时(页面展示)

解决方式:

  • 单纯页面展示的话,做一个监听事件,当play()开启即可计时
  • 其他用处?

2、流合并(电话+录屏/视屏通话+录屏)

https://github.com/muaz-khan/MultiStreamsMixer

3、通讯文件生成&上传

五、其他可用性

1、自定关闭按钮

解决方式:写一个监听事件处理,避免监控段看到暂停画面

      openLocalScream() {
        return new Promise((resolve, reject) => {
          // 摄像头(二选一)
          // navigator.mediaDevices.getUserMedia(this.mediaConstraints)
          // 屏幕共享
          navigator.mediaDevices.getDisplayMedia(this.screenConstraints)
                  .then((stream) => {
                    this.localMediaStream = stream;
                    let localScream = document.getElementById("localScream");
                    localScream.srcObject = this.localMediaStream;
                    localScream.play();
                  })
                  .then(() => {console.log("打开本地音视频设备成功")
                    //关闭共享事件监听
                    this.localMediaStream.getVideoTracks()[0].addEventListener('ended', () => console.log('停止屏幕共享 '))})
                  .catch(() => console.log("打开本地音视频设备失败"));
        });
      },

2、清晰度/分辨率

  • 分辨率固定
//录屏参数
screenConstraints: {
  video: {
    cursor: 'always' | 'motion' | 'never',
    displaySurface: 'window' | 'browser' | 'monitor' | 'window',
    width: 1920,
    height: 1080
  }
},
//视频参数
mediaConstraints: {
  video: {
    width: 1920,
    height: 1080
  },
  //开始语音
  audio: true
},
  • 分辨率约束(目前demo中screenConstraints不可用下列配置,会发生异常)
//1. 指定值
const constraints = {
	width: 1280,
	height: 720,
	aspectRatio: 3/2
};

//2. 指定最小值和理想值
const constraints = {
	frameRate: {min: 20},
	width:{min: 640, ideal: 1280},
	height:{min: 480, ideal: 720},
	aspectRatio: 3/2
};

//3.指定最小值、理想值和最大值
const constraints = {
	width: {min: 320, ideal: 1280, max: 1920},
	height: {min: 240, ideal: 720, max: 1080},
}

参考第二章约束

  • 分辨率动态调整(?)

3、流畅度(影响要素?)

(1)传输速率
  • SDP修改

    Chrome默认将WebRTC的数据通道传输速度限制在30K

    https://blog.csdn.net/holdsky/article/details/120841013?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-0-120841013-blog-124912802.pc_relevant_multi_platform_whitelistv3&spm=1001.2101.3001.4242.1&utm_relevant_index=3

六、参考链接

WebRTC常用的API详解和SDP协商过程 | Manoner的学习笔记

SDP修改问题

WebRtc入门 - API说明_fun binary的博客-CSDN博客_webrtc接口文档

WebRTC音视频原理_小油酱的博客-CSDN博客

webrtc分辨率和比特率–问题集锦-蒲公英云 (dandelioncloud.cn)

Webrtc及WEB端音视频设备获取及流处理 | 码客 (psvmc.cn)

webrtc 入门第二章 音视频录制_日落班的博客-CSDN博客_webrtc录像

MediaDevices.getUserMedia() - Web API 接口参考 | MDN (mozilla.org)

webrtc之onicecandidate的 event handler的一点疑惑_会飞的胖达喵的博客-CSDN博客_onicecandidate什么时候调用

何时调用RTCPeerConnection.onIcecandidate()事件? - 问答 - 腾讯云开发者社区-腾讯云 (tencent.com)

Web实时语音/视频聊天/文件传输 - 掘金 (juejin.cn)

关于webRTC的学习记录 - 掘金 (juejin.cn)

实时音视频WebRTC介绍 - 腾讯云开发者社区-腾讯云 (tencent.com)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值