WebRTC源码研究(31)本机内的1对1音视频互通

WebRTC源码研究(31)本机内的1对1音视频互通

1. 1对1连接过程

我们先来回顾一下:
在这里插入图片描述

这张图看起来复杂,但是基本上可以分成三大块:

  • 第一大块就是整个媒体的协商,看A端有什么媒体能力看B端有什么媒体能力,他们直接所有的媒体取一个交集,取大家都能够识别的支持的能力,包括音频编解码视频编解码,这个采样率是多少,帧率是多少,以及网络的一些信息;
  • 第二大部分就是通过ICE对整个可连通的链路进行这个链路地址的收集,它收集完了之后进行排序和连接检测,找出双方可以连接的最优的这条线路,
  • 最后拿到线路之后就可以进行媒体数据的传输了,当从一端传输到另一端之后呢,另一端会收到一个事件,就是onAddStream,当收到这个事件之后就可以将这个媒体流添加到自己的video标签audio标签中进行音频的播放和视频的渲染,这个就是整个端对端连接的基本流程。

今天我们来具体实操一下,看看上面具体的这些过程,通过代码如何取控制整个的逻辑,我们实操这个的例子中,为了尽量的简化尽量的容易,

我们并没有使用真实的跨网络,是从一台电脑的网络到另一台电脑的网络进行真实的音视频的传输,

而是在我们一个页面里面其中一个video取展示我们本地采集的音频和视频,那之后创建两个PeerConnection,然后将这个媒体流加入到其中一个PeerConnection之后让他们进行连接,连接之后进行本机底层的网络传输,传到另一端的PeerConnection,当另一端PeerConnection收到这个音视频数据之后取回调这个事件,也就是onAddStream,

当另一端收到这个onAddStream之后,将这个收到的数据转给视频标签,视频就被渲染出来了,虽然没有经过真实的网络,但是他们整体的流程和真实网络的流程是一摸一样的,

虽然不用信令传输了,但是我们还是要走这样一个逻辑,主要是为了让大家感受到我们整套这个逻辑都是按照我们上节所讲的过程处理的,在后面我们就会将真实的网络加入进去,那么大家就会看到实际整个流程并没有变,只是把这个信令通过信令服务器进行中转了,那么你这个网络连接不是在自己本地转了 ,而是通过真实的网络进行传输了。

下面我们就来实际操作一番

和以前的例子其实是一样的,它也包括了展示部分和控制部分,展示部分就是HTML,控制部分就是利用JS去调用webrtcAPI实现我们整个的流程。

2. 本机内的1:1音视频互通实战

这个例子跟之前一样包含两个部分:显示部分和控制部分。
显示部分就是一个html网页,控制部分就是我们的js代码。

2.1 HTML部分

我们先命令行通过Vi 命令创建一个index.html

输入如下内容:
在这里插入图片描述

2.2 JS部分

新建一个main.js 文件:

我们先定义一个变量接收html的控件,并绑定按钮响应事件

js

接下来我们实现具体的js函数:

首先是start()函数

function start(){
	// 11、在调getUserMedia之前我们还有一个限制,是constraints
	// 这这里面我们可以写video和audio,我们这里暂不采集音频了,如果你要采集设置audio为true好了
	var constraints = {
		video: true,
		audio: false 
	}
	// 7、判断浏览器是否支持navigator.mediaDevices 和 navigator.mediaDevices.getUserMedia
	// 如果有任何一个不支持我们就要打印一个错误信息并退出
	if(!navigator.mediaDevices ||
		!navigator.mediaDevices.getUserMedia){
		console.error('the getUserMedia is not supported!')
		return;
	}else { // 8、如果支持就要传人constraints使用getUserMedia获取音视频数据了
		navigator.mediaDevices.getUserMedia(constraints)
					.then(gotMediaStream) // 9、如果成功我们就让他调gotMediaStream,这时候说明我们可以拿到这个stream了
					.catch(handleError); // 10、否则就要处理这个错误
	}
}

此外还有获取流的函数:

// 12、在我们采集成功之后我们会调用gotMediaStream,它有个参数就是stream
function gotMediaStream(stream){
	// 13、在这个stream里面要做两件事
	// 14、第一件事就是将它设置为localVideo,这样采集到数据之后我们本地的localVideo就能将它展示出来
	localVideo.srcObject = stream;
	/** 15、第二件事就是我们要将它赋值给一个全局的stream,这个localStream就是为了后面我们去添加流用的,就是我们在其他地方还要用到这个流,
				  所以我们不能让它是一个局部的,得让他是一个全局的 */
	localStream = stream;
}

处理错误处理函数:

// 17、处理错误
function handleError(err){
	// 18、它有个参数err,当我们收到这个错误我们将它打印出来
	console.log("Failed to call getUserMedia", err);
}

我们可以先调试预览一下效果:
单步调试一下

接下来我们实现call()函数

// 19、实现call
function call(){
	// 20、这个逻辑略显复杂,首先是创建RTCPeerConnection
	/**
	 * 38、创建offerOptions,那在这里你可以指定我创建我本地的媒体的时候,那都包括哪些信息,
	 * 可以有视频流和音频流,因为我们这里没有采集音频所以offerToReceiveAudio是0
	 * 有了这个之后我们就可以创建本地的媒体信息了
	 */
	var offerOptions = {
		offerToReceiveAudio: 0,
		offerToReceiveVideo: 1 
	}
	/** 21、所以在这里先设置一个pc1等于new RTCPeerConnection()
	   在这个Connection里面实际上是有一个可选参数的,这个可选参数就涉及到网络传输的一些配置
		 我们整个ICE的一个配置,但是由于是我们在本机内进行传输,所以在这里我们就不设置参数了,因为它也是可选的
		 所以它这里就会使用本机host类型的candidate
		 这个pc1我们后面也要用到,所以我给他全局化一下
		 */
	pc1 = new RTCPeerConnection();
	pc1.onicecandidate = (e) => {
		// 25、这些事件有几个重要的,那第一个因为这个是连接,连接建立好之后当我发送收集Candidate的时候,那我要知道现在已经收到一个了
		// 收到之后我们要做一些事情,所以我们要处理addIceCandidate事件,其实做主要的就是这个事件
		// send candidate to peer
		// receive candidate from peer
		/** 
		 * 27、它有个参数就是e,当有个事件的时候这个参数就传进来了,对于我们上面的逻辑,我们回顾一些此前的流程 
		 * 当我们A调用者收到candidate之后,它会将这个candidate发送给这个信令服务器
		 * 那么信令服务器会中转到这个B端,那么这个B端会调用这个AddIceCandidate这个方法,将它存到对端的candidate List里去
		 * 所以整个过程就是A拿到它所有的可行的通路然后都交给B,B形成一个列表;
		 * 那么B所以可行的通路又交给A,A拿到它的可行列表,然后双方进行这个连通性检测
		 * 那么如果通过之后那就可以传数据了,就是这样一个过程
		 * */
		/** 28、所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的这个,因为是本机这里就没有信令了,假设信令被传回来了
		 * 当我们收集到一个candidate之后交给信令,那么信令传回来,这时候就给了pc2
		*/
		/**
		 * 29、pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate
		 */
		pc2.addIceCandidate(e.candidate)
			.catch(handleError);
		console.log('pc1 ICE candidate:', e.candidate);
	}
 
	pc1.iceconnectionstatechange = (e) => {
		console.log(`pc1 ICE state: ${pc.iceConnectionState}`);
		console.log('ICE state change event: ', e);
	}
 
	// 24、在创建一个pc2这样我们就创建了两个连接,拿到两个连接之后我们要添加一些事件给它
	// 26、对于pc2还要处理一个onTrack,当双方通讯连接之后,当有流从对端过来的时候,会触发这个onTrack事件,所以这个事件是我们要处理的
	pc2 = new RTCPeerConnection();
	/** 
	 * 30、对于pc2也是同样道理,那它就交给p1
	 */
	pc2.onicecandidate = (e)=> {
	
		// send candidate to peer
		// receive candidate from peer
		/**
		 * 31、所以它就调用pc1.addIceCandidate,这是当他们收集到candidate之后要做的事情
		 */
		pc1.addIceCandidate(e.candidate)
			.catch(handleError);
		console.log('pc2 ICE candidate:', e.candidate);
	}
	pc2.iceconnectionstatechange = (e) => {
		console.log(`pc2 ICE state: ${pc.iceConnectionState}`);
		console.log('ICE state change event: ', e);
	}
	/**
	 * 32、除此之外,pc2是被调用方,被调用方是接收数据的,所以对于pc2它还有个ontrack事件
	 * 当它收到这个ontrack事件之后它需要调用gotRemoteStream
	 */
	pc2.ontrack = gotRemoteStream;
	/** 
	 *  36、接下来我们就要将本地采集的数据添加到第一添加到第一个pc1 = new RTCPeerConnection()中去
	 *  这样在创建媒体协商的时候才知道我们有哪些媒体数据,这个顺序不能乱,必须要先添加媒体数据再做后面的逻辑
	 *  二不能先做媒体协商然后在添加数据,因为你先做媒体协商的时候它知道你这里没有数据那么在媒体协商的时候它就没有媒体流
	 * 那么就是说在协商的时候它知道你是没有的,那么它在底层就不设置这些接收信息发收器,那么这个时候即使你后面设置了媒体流传给这个PeerConnection
	 * 那么它也不会进行传输的,所以我们要先添加流
	 * 添加流也比较简单,通过localStream调用getTracks就能调用到所有的轨道(音频轨/视频轨)
	 * 那对于每个轨道我们添加进去就完了,也就是forEach遍历进去,每次循环都能拿到一个track
	 * 当我们拿到这个track之后直接调用pc1.addTrack添加就好了,第一个参数就是track,第二个参数就是这个track所在的流localStream
	 * 这样就将本地所采集的音视频流添加到了pc1 这个PeerConnection
	*/
	//add Stream to caller
	localStream.getTracks().forEach((track)=>{
		pc1.addTrack(track, localStream);
	});
	/**
	 * 37、那么这个时候我们就可以去创建这个pc1去媒体协商了,媒体协商第一步就是创建createOffer,创建这个createOffer实际它里面有个
	 * offerOptions的,那么这个offerOptions我们在上面定义一下
	 */
	pc1.createOffer(offerOptions)
		.then(gotLocalDescription) // 39、它也是一个Promise的形式,当他成功的时候我们去调用gotLocalDescription
		.catch(handleError); // 40、如果失败了去调用一下handleError
 
}

2.3 完整代码

完整代码

// 1、使用JS严格语法
'use strict'
 
// 2、获取HTML中的元素标签
var localVideo = document.querySelector('video#localVideo');
var remoteVideo = document.querySelector('video#remoteVideo');
var btnStart = document.querySelector('button#start');
var btnCall = document.querySelector('button#call');
var btnHangUp= document.querySelector('button#hangup');
 
// 16、定义全局变量localStream
var localStream;
// 22、定义全局变量pc1
var pc1;
// 23、定义全局变量pc2
var pc2;
 
// 12、在我们采集成功之后我们会调用gotMediaStream,它有个参数就是stream
function gotMediaStream(stream){
	// 13、在这个stream里面要做两件事
	// 14、第一件事就是将它设置为localVideo,这样采集到数据之后我们本地的localVideo就能将它展示出来
	localVideo.srcObject = stream;
	/** 15、第二件事就是我们要将它赋值给一个全局的stream,这个localStream就是为了后面我们去添加流用的,就是我们在其他地方还要用到这个流,
				  所以我们不能让它是一个局部的,得让他是一个全局的 */
	localStream = stream;
}
 
// 17、处理错误
function handleError(err){
	// 18、它有个参数err,当我们收到这个错误我们将它打印出来
	console.log("Failed to call getUserMedia", err);
}
 
// 6、实现start
function start(){
	// 11、在调getUserMedia之前我们还有一个限制,是constraints
	// 这这里面我们可以写video和audio,我们这里暂不采集音频了,如果你要采集设置audio为true好了
	var constraints = {
		video: true,
		audio: false 
	}
	// 7、判断浏览器是否支持navigator.mediaDevices 和 navigator.mediaDevices.getUserMedia
	// 如果有任何一个不支持我们就要打印一个错误信息并退出
	if(!navigator.mediaDevices ||
		!navigator.mediaDevices.getUserMedia){
		console.error('the getUserMedia is not supported!')
		return;
	}else { // 8、如果支持就要传人constraints使用getUserMedia获取音视频数据了
		navigator.mediaDevices.getUserMedia(constraints)
					.then(gotMediaStream) // 9、如果成功我们就让他调gotMediaStream,这时候说明我们可以拿到这个stream了
					.catch(handleError); // 10、否则就要处理这个错误
	}
 
}
// 45、这样也会传入一个描述信息
function gotAnswerDescription(desc){
	// 46、当远端它得到这个Answer之后,它也要设置它的setLocalDescription,当它调用了setLocalDescription之后它也开始收集candidate了
	pc2.setLocalDescription(desc);
 
	//send sdp to caller
	//recieve sdp from callee
	/** 47、完了之后它去进行send desc to signal与pc1进行交换,pc1会接收recieve desc from signal
			那么收到之后他就会设置这个pc1的setRemoteDescription
			那么经过这样一个步骤整个协商就完成了
			当所有协商完成之后,这些底层对candidate就收集完成了
			收集完了进行交换形成对方的列表然后进行连接检测
			连接检测完了之后就开始真正的数据发送过来了
	*/
	pc1.setRemoteDescription(desc);
 
}
// 41、首先传入一个desc一个描述信息
function gotLocalDescription(desc){
	/** 42、当我们拿到这个描述信息之后呢,还是回到我们当时协商的逻辑,对于A来说它首先创建Offer,创建Offer之后它会调用setLocalDescription
	 * 将它设置到这个PeerConnection当中去,那么这个时候它会触发底层的ICE的收集candidate的这个动作
	 * 所以这里要调用pc1.setLocalDescription这个时候处理完了它就会收集candidate
	 * 这个处理完了之后按照正常逻辑它应该send desc to signal到信令服务器
 	*/
	pc1.setLocalDescription(desc);
 
	//send sdp to callee
	/**
	 * 43、到了信令服务器之后,信令服务器会发给第二个人(b)
	 * 所以第二个人就会receive
	 * 所以第二个人收到desc之后呢首先pc2要调用setRemoteDescription,这时候将desc设置成它的远端
	 */
	//receive sdp from caller 
	pc2.setRemoteDescription(desc);	
	/**
	 * 44、设成远端之后呢它也要做它自己的事
	 * pc2就要调用createAnswer,如果调用createAnswer成功了它要调用gotAnswerDescription
	 */ 
	pc2.createAnswer().then(gotAnswerDescription)
			 .catch(handleError);
}
/**
 * 33、在这个函数里它实际就是有多个流了,
 */
function gotRemoteStream(e){
	// 34、所以我们只要取其中的第一个就可以了
	if(remoteVideo.srcObject !== e.streams[0]){
		// 35、这样我们就将远端的音视频流传给了remoteVideo,当发送ontrack的时候也就是数据通过的时候,
		remoteVideo.srcObject = e.streams[0];
	}
}
// 19、实现call
function call(){
	// 20、这个逻辑略显复杂,首先是创建RTCPeerConnection
	/**
	 * 38、创建offerOptions,那在这里你可以指定我创建我本地的媒体的时候,那都包括哪些信息,
	 * 可以有视频流和音频流,因为我们这里没有采集音频所以offerToReceiveAudio是0
	 * 有了这个之后我们就可以创建本地的媒体信息了
	 */
	var offerOptions = {
		offerToReceiveAudio: 0,
		offerToReceiveVideo: 1 
	}
	/** 21、所以在这里先设置一个pc1等于new RTCPeerConnection()
	   在这个Connection里面实际上是有一个可选参数的,这个可选参数就涉及到网络传输的一些配置
		 我们整个ICE的一个配置,但是由于是我们在本机内进行传输,所以在这里我们就不设置参数了,因为它也是可选的
		 所以它这里就会使用本机host类型的candidate
		 这个pc1我们后面也要用到,所以我给他全局化一下
		 */
	pc1 = new RTCPeerConnection();
	pc1.onicecandidate = (e) => {
		// 25、这些事件有几个重要的,那第一个因为这个是连接,连接建立好之后当我发送收集Candidate的时候,那我要知道现在已经收到一个了
		// 收到之后我们要做一些事情,所以我们要处理addIceCandidate事件,其实做主要的就是这个事件
		// send candidate to peer
		// receive candidate from peer
		/** 
		 * 27、它有个参数就是e,当有个事件的时候这个参数就传进来了,对于我们上面的逻辑,我们回顾一些此前的流程 
		 * 当我们A调用者收到candidate之后,它会将这个candidate发送给这个信令服务器
		 * 那么信令服务器会中转到这个B端,那么这个B端会调用这个AddIceCandidate这个方法,将它存到对端的candidate List里去
		 * 所以整个过程就是A拿到它所有的可行的通路然后都交给B,B形成一个列表;
		 * 那么B所以可行的通路又交给A,A拿到它的可行列表,然后双方进行这个连通性检测
		 * 那么如果通过之后那就可以传数据了,就是这样一个过程
		 * */
		/** 28、所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的这个,因为是本机这里就没有信令了,假设信令被传回来了
		 * 当我们收集到一个candidate之后交给信令,那么信令传回来,这时候就给了pc2
		*/
		/**
		 * 29、pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate
		 */
		pc2.addIceCandidate(e.candidate)
			.catch(handleError);
		console.log('pc1 ICE candidate:', e.candidate);
	}
 
	pc1.iceconnectionstatechange = (e) => {
		console.log(`pc1 ICE state: ${pc.iceConnectionState}`);
		console.log('ICE state change event: ', e);
	}
 
	// 24、在创建一个pc2这样我们就创建了两个连接,拿到两个连接之后我们要添加一些事件给它
	// 26、对于pc2还要处理一个onTrack,当双方通讯连接之后,当有流从对端过来的时候,会触发这个onTrack事件,所以这个事件是我们要处理的
	pc2 = new RTCPeerConnection();
	/** 
	 * 30、对于pc2也是同样道理,那它就交给p1
	 */
	pc2.onicecandidate = (e)=> {
	
		// send candidate to peer
		// receive candidate from peer
		/**
		 * 31、所以它就调用pc1.addIceCandidate,这是当他们收集到candidate之后要做的事情
		 */
		pc1.addIceCandidate(e.candidate)
			.catch(handleError);
		console.log('pc2 ICE candidate:', e.candidate);
	}
	pc2.iceconnectionstatechange = (e) => {
		console.log(`pc2 ICE state: ${pc.iceConnectionState}`);
		console.log('ICE state change event: ', e);
	}
	/**
	 * 32、除此之外,pc2是被调用方,被调用方是接收数据的,所以对于pc2它还有个ontrack事件
	 * 当它收到这个ontrack事件之后它需要调用gotRemoteStream
	 */
	pc2.ontrack = gotRemoteStream;
	/** 
	 *  36、接下来我们就要将本地采集的数据添加到第一添加到第一个pc1 = new RTCPeerConnection()中去
	 *  这样在创建媒体协商的时候才知道我们有哪些媒体数据,这个顺序不能乱,必须要先添加媒体数据再做后面的逻辑
	 *  二不能先做媒体协商然后在添加数据,因为你先做媒体协商的时候它知道你这里没有数据那么在媒体协商的时候它就没有媒体流
	 * 那么就是说在协商的时候它知道你是没有的,那么它在底层就不设置这些接收信息发收器,那么这个时候即使你后面设置了媒体流传给这个PeerConnection
	 * 那么它也不会进行传输的,所以我们要先添加流
	 * 添加流也比较简单,通过localStream调用getTracks就能调用到所有的轨道(音频轨/视频轨)
	 * 那对于每个轨道我们添加进去就完了,也就是forEach遍历进去,每次循环都能拿到一个track
	 * 当我们拿到这个track之后直接调用pc1.addTrack添加就好了,第一个参数就是track,第二个参数就是这个track所在的流localStream
	 * 这样就将本地所采集的音视频流添加到了pc1 这个PeerConnection
	*/
	//add Stream to caller
	localStream.getTracks().forEach((track)=>{
		pc1.addTrack(track, localStream);
	});
	/**
	 * 37、那么这个时候我们就可以去创建这个pc1去媒体协商了,媒体协商第一步就是创建createOffer,创建这个createOffer实际它里面有个
	 * offerOptions的,那么这个offerOptions我们在上面定义一下
	 */
	pc1.createOffer(offerOptions)
		.then(gotLocalDescription) // 39、它也是一个Promise的形式,当他成功的时候我们去调用gotLocalDescription
		.catch(handleError); // 40、如果失败了去调用一下handleError
 
}
// 48、挂断,将pc1和pc2分别关闭,在将pc1和pc2对象设为null
function hangup(){
	pc1.close();
	pc2.close();
	pc1 = null;
	pc2 = null;
}
 
// 3、开始
btnStart.onclick = start;
// 4、当调用call的时候就会调用双方的RTCPeerConnection,
// 当这个两个PeerConnection创建完成之后,它们会作协商处理,
// 协商处理晚上之后进行Candidate采集,也就是说有效地址的采集,
// 采集完了之后进行交换,然后形成这个Candidate pair再进行排序,
// 然后再进行连接性检测,最终找到最有效的那个链路,
// 之后就将localVideo展示的这个数据通过PeerConnection传送到另一端,
// 另一端收集到数据之后会触发onAddStream或者onTrack就是说明我收到数据了,那当收到这个事件之后,
// 我们再将它设置到这个remoteVideo里面去,
// 这样远端的这个video就展示出来了,显示出我们本地采集的数据了,
// 大家会看到两个视频是一摸一样的,它的整个底层都是从本机自己IO的那个逻辑网卡转过来的,
// 不过这个整个流程都是一样的,当我们完成这个,在做真实的网络传输的时候就和这个一摸一样,只是将信令部分加了进来
btnCall.onclick = call;
// 5、挂断
btnHangUp.onclick = hangup;


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值