系列文章目录
webrtc学习记录一【媒体录制MediaRecorder】
webrtc学习记录二【基于socket.io创建信令服务器聊天室】
目录
1.2、RTCpeerConnection媒体协商的实例方法
1.3、RTCpeerConnection媒体协商的实例的事件。
前言
记录webrtc学习过程中的要点,以便温故知新。本章主要是基于RTCPeerConnection,创建一套本机内的1v1音视频系统,没有通过UDP服务器和信令服务器的中转。
提示:以下是本篇文章正文内容,下面案例可供参考
一、媒体能力的协商过程
上图模拟了A和B进行互通时的媒体协商过程,
A需要创建一个offer信息,然后将offer信息通过setLocalDescription设置给peerConnection A;同时,发送offer到信令服务器。
信令服务器接收到A发送过来的offer之后,转发给B,B将来自A的offer信息通过setRemoteDescription设置到远端连接(因为对于B来说,A发送来的信息就是远端)
然后B创建一个answer,然后将answer信息通过setLocalDescription设置给peerConnection B;同时,发送answer信息到信令服务器。
信令服务器接收到B发送过来的answer之后,转发给A,A将来自B的answer信息通过setRemoteDescription设置到远端连接(因为对于A来说,B发送来的信息就是远端)
这样A和B的peerConnection就互相拿到了对方的sdp信息,从而能够实现互通。
1.1、RTCpeerConnection类的使用
上述流程的实现都是基于类RTCpeerConnection,因此下面就开始介绍一下它的用法和相关api
基本格式
var myPeerConnection = new RTCPeerConnection([configuration])
参数说明
参数 | 说明 |
configuration | 一个对象集合,包含许多配置项。一般主要配置ice服务器地址点对点连接。 |
configuration详细说明
configuration的详细参数包含如下:
参数 | 说明 |
iceServers | 最关键的一项,设置STUN 或 TURN服务的地址。不填则限制为本地 |
iceTransportPolicy | 指定ICE传输策略: relay:只是用中继候选者 all:可以使用任何类型的候选者,包含中继,host类型。但是国内网络基本只有中继能连通、 |
bundlePolicy | 'balanced',音轨与视轨分别使用音频和适配的通道,如2个音轨2个视轨,只要2个通道。 'max-compat',每个轨使用自己的传输通道,如3个视轨需要3个通道 'max-bundle',将音轨和视轨都绑定到同一个传输通道,只使用1个通道 |
rtcpMuxpolicy | 'negotiate',收集RTCP与复用RTP复用的ICE候选者,如果RTCP能复用就与RTP复用,如果不能复用,就将他们单独使用。 'require',只能收集RTCP与RTP复用的ICE候选者,如果RTCP不能复用,则失败。 默认是'require' |
peerIdentity | 建立对等连接时的标识字符串,可以不设置,默认是null |
certificates | 授权可以使用连接的一组证书,不设置会默认提供一个。 每一个可连通的候选者都需要一个证书,复用情况下有1个即可。 |
iceCandidatePoolSize | 16位的整数值,用于指定预取的ICE候选者的个数。 默认值为 0(意味着不会发生候选预取) 改变 ICE 候选池的大小可能会触发 ICE 收集的开始。 |
iceServers详细说明
数组类型,可以设置多个STUN或TURN服务地址,每一个都是一个ICE代理的服务。
属性 | 含义 |
credential | 凭据,只有TURN服务使用 |
credentialType | 凭据类型,可以是password或者oauth |
urls | 用于连接服务中的url数组 |
username | 用户名,只有TURN服务使用 |
常见配置示例:
const peerConnectionConfig = {
'iceServers': [
{
'urls': 'stun:stun.l.google.com:19302',
'credential': 'mypasswd',
'username': 'myusername'
}
]
}
const myPeerConnection = new RTCPeerConnection(peerConnectionConfig )
1.2、RTCpeerConnection媒体协商的实例方法
以上面创建的myPeerConnection 实例为例,下面介绍一些必须的示例方法。
createOffer
基本格式
aPromise = myPeerConnection.createOffer([offerOptions])
offerOptions
属性 | 含义 |
offerToRecieveAudio | 0或者1,0表示不传输音频,1表示传输音频 |
offerToRecieveVideo | 0或者1,0表示不传输视频,1表示传输视频 |
voiceActivityDetection | true/false,默认true, 是否开启静音检测,默认开启。不说话时过滤背景音。 |
iceRestart | true/false, 该选项会重启ICE,重新进行Candidate收集。可以查看sdp中的ufrag信息判断,对于同一个prconnection,false的时候,每次createOfferufrag不变,true则每次都变化。 缺点是消耗带宽,优点是反馈及时,网络变化时也能确保联通。 |
示例
myPeerConnection.createOffer({
offerToRecieveAudio: 0,
offerToRecieveVideo: 1,
}).then(desc => {
}).catch(err => console.log('本地offer创建失败', err))
createAnswer
基本格式
aPromise = myPeerConnection.createAnswer([answerOptions])
示例
this.pcRemote.createAnswer().then(desc => {
}).catch(err => console.log('远端answer创建失败', err))
setLocalDescription
方法中的sessionDescription取的就是offer和answer中返回的desc
基本格式
aPromise = myPeerConnection.setLocalDescription(sessionDescription)
示例
this.pcLocal.createOffer(offerOptions).then(desc => {
this.pcLocal.setLocalDescription(desc)
}).catch(err => console.log('本地offer创建失败', err))
setRemoteDescription
基本格式
aPromise = myPeerConnection.setRemoteDescription(sessionDescription)
示例
this.pcRemote.createAnswer().then(desc => {
this.pcRemote.setRemoteDescription(desc)
}).catch(err => console.log('远端answer创建失败', err))
addTrack
基本格式
rtpSender = myPeerConnection.addTrack(track,stream...)
参数说明
属性 | 说明 |
track | 添加到RTCPeerConnerction中的媒体轨 |
sdpMid | 指定track所在的stream |
示例:
this.localStream.getTracks().forEach(track => {
this.pcLocal.addTrack(track, this.localStream)
})
这里的localStream来自navigator.mediaDevices.getUserMedia,第一章的学习记录中有用到。
removeTrack
对应addTrack,移除轨道。
基本格式
myPeerConnection.removeTrack(rtpSender)
addIceCandidate
基本格式
aPromise = myPeerConnection.addIceCandidate(candidate)
candidate属性
属性 | 说明 |
candidate | 候选者描述信息 |
sdpMid | 与候选者相关的媒体流的识别标签 |
sdpMLineIndex | 在SDP中m=的索引值 |
usernameFragment | 包括了远端的唯一标识 |
1.3、RTCpeerConnection媒体协商的实例的事件。
onnegotiationneeded
进行媒体协商的时候触发的事件。
onicecandidate
当收到ice候选者的时候触发。
ontrack
媒体轨道添加的时候触发。
二、端对端连接的基本流程
了解了上述api之后,我们再详细的分解一下端对端连接的基本流程,为后续开发做好准备。
- 首先是A和B分别连接信令服务器signal,为后续通讯做准备。
- 然后A创建peerConnectionA连接,同时添加媒体流到本地视频窗口,以便显示本地视频内容。
- A的peerConnectionA连接创建完成后,开始创建offer,然后将offer信息设置到A的peerConnectionA中的setLocalDescription,并且A会对stun/turn服务器发起请求,期望stun/turn服务器返回A解析后的的候选者信息candidateA,同时将offer SDP信息发送给信令服务器。
- 信令服务器收到A的offer SDP信息后,马上通过信令服务器传给B。这时B需要先创建B的peerConnectionB,然后将前面信令服务器传过来的offer SDP通过setRemoteDescription设置到远端描述中(因为对于B来说,A传过来的信息就是远端信息)。接下来B开始创建answer,然后将B创建的answer SDP信息通过setLocalDescription设置到peerConnectionB的本地描述信息中。然后将B的candidate信息发送给stun/turn服务器上。同时将answer SDP信息发送给信令服务器。
- 信令服务器将answer SDP信息返回给A,A收到后将answer SDP信息通过setRemoteDescription设置到A的远端描述中。
- 当A收到了来自ICE服务器中返回的peerConnectionA的候选者信息candidateA后,将candidateA发送给信令服务器。
- 信令服务器接收到candidateA后,发送给peerConnectionB,peerConnectionB通过addIceCandidate方法,将candidateA设置给peerConnectionB
- 当B收到了来自ICE服务器中返回的peerConnectionB的候选者信息candidateB后,将candidateB发送给信令服务器。
- 信令服务器接收到candidateB后,发送给peerConnectionA,peerConnectionA通过addIceCandidate方法,将candidateB设置给peerConnectionA
- 至此,peerConnectionA和peerConnectionB就分别获取到了对方的candidate候选者信息,peerConnection的底层就会通过排序,链接检测,找到一个A和B之间的最优通信线路,这样就建立好了一个A和B之间的P2P通道。
- 最后通过ontrack事件,监听接收到的A的媒体流,展示到远端。这样A和B之间的p2p音视频互通就基本实现了。
三、实战,创建一套本机内的1:1音视频互通。
由于是 本机内互通,所以信令服务器和stun服务器中转的步骤全部可以忽略。
new RTCPeerConnection([configuration])中的configuration为空时,默认采用本机host来解析,所以我们可以直接拿到candidate
因此上述流程图修改成这样,蓝色线表示修改后的,可能有点乱,大家将就着看一下吧。。。o(╯□╰)o
看下最终需要实现的效果图:
页面基础结构
先写好dom结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>peerconnection点对点音视频互通(不含信令服务器)</title>
<style>
.btn_wrap{
display: flex;
}
.btn{
margin-right: 15px;
}
.video_wrap{
display: flex;
}
.col{
margin-right: 20px;
display: flex;
flex-direction: column;
}
.sdp{
width: 200px;
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<div class="btn_wrap">
<button class="btn" @click="startCollect">开始采集数据,只看的到本地</button>
<button class="btn" @click="call">呼叫,能看到远端</button>
<button class="btn" @click="hangup">挂断</button>
</div>
<div class="video_wrap">
<div class="left col">
<span>本地视频流:</span>
<video :width="videoSize" :height="videoSize" autoplay ref="localVideo"></video>
<span>offer SDP</span>
<textarea class="sdp" readonly :value="offerSDP" cols="30" rows="10"></textarea>
</div>
<div class="right col">
<span>远端视频流:</span>
<video :width="videoSize" :height="videoSize" autoplay ref="remoteVideo"></video>
<span>answer SDP</span>
<textarea class="sdp" readonly :value="answerSDP" cols="30" rows="10"></textarea>
</div>
</div>
</div>
</div>
</body>
<script src="./js/socket.io.js"></script>
<script src="./js/adapter-latest.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
answerSDP: '',
offerSDP: '',
pcLocal: null,
pcRemote: null,
localStream : null,
constraints: {
audio: false,
video: true
},
videoSize:200,
},
methods: {
},
created() {
console.log('io', io)
},
})
</script>
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init()</script>
</html>
采集本地数据流
先实现简单功能点,点击第一个按钮采集本地数据,这样本地video窗口就能显示。对应学习记录一中的内容。
/**
* 开始采集,收集本地媒体流信息,并展示。
*/
startCollect() {
if(!window.navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.log('the getUserMedia is not supported!')
return false
} else {
window.navigator.mediaDevices.getUserMedia(this.constraints).then(stream => {
this.localStream = stream
this.$refs.localVideo.srcObject = stream
console.log('this.$refs.localVideo.srcObject', this.$refs.localVideo.srcObject)
}).catch(err => {
console.log('音视频采集错误!', err)
})
}
},
连接远端
点击第二个按钮,调用call函数,连接远端。
因为这里我们只模拟本机内的1v1互通,所以不需要信令服务器来中转,也不需要stun/trun服务器解析。
创建peerConnection
先分别创建本地和远端的peerConnection
this.pcLocal = new RTCPeerConnection()
this.pcRemote = new RTCPeerConnection()
添加媒体流到peerConnection
由于媒体流可能有多个,所以这里是一个数组结构,遍历后将每一个媒体轨道都添加上对应的媒体流。
this.localStream.getTracks().forEach(track => {
this.pcLocal.addTrack(track, this.localStream)
})
本地端开始创建offer
createOffer返回的是一个promise,所以我们直接用then操作来处理后续获得offer之后的处理。
const offerOptions = {
offerToRecieveAudio: 0,
offerToRecieveVideo: 1,
}
this.pcLocal.createOffer(offerOptions).then(getOffer).catch(err => console.log('本地offer创建失败', err))
处理offer信息
在前面的步骤拿到offer信息后,马上开始设置描述信息,发送sdp,创建answer
对应流程图中的这一步
const getOffer = desc => {
this.pcLocal.setLocalDescription(desc)
this.offerSDP = desc.sdp
// send desc to signal
// pcRemote receive desc from signal
this.pcRemote.setRemoteDescription(desc)
this.pcRemote.createAnswer().then(getanswer).catch(err => console.log('远端answer创建失败', err))
}
处理answer信息
const getanswer = desc => {
this.pcRemote.setLocalDescription(desc)
this.answerSDP = desc.sdp
// send desc to signal
// pcLocal receive desc from signal
this.pcLocal.setRemoteDescription(desc)
console.log('getanswer')
}
处理onicecandidate接收事件
虽然没有stun/trun服务器,但是RTCPeerConnection会默认采集本机host信息来解析candidate,所以依然会有onicecandidate事件。
// 这几个事件是异步回调。
/**
*
*/
this.pcLocal.onicecandidate = e => {
// pcLocal send candidate to signal
// pcRemote revieve candidate from signal
console.log('pcLocal.onicecandidate')
this.pcRemote.addIceCandidate(e.candidate)
}
this.pcRemote.onicecandidate = e => {
console.log('pcRemote.onicecandidate')
this.pcLocal.addIceCandidate(e.candidate)
}
添加远端流
最后,通过ontrack事件监听媒体轨是否添加成功,添加成功后将媒体流信息输出到界面上即可。
this.pcRemote.ontrack = e => {
console.log('pcRemote.onTrack')
this.$refs.remoteVideo.srcObject = e.streams[0]
}
完整代码见git地址:
https://github.com/Silent-Jude/webRtcLearning/blob/main/public/peerconnection_local/index.html