大家好,欢迎来到IT知识分享网。
一:媒体能力协商
(一)RTCPeerConnection回顾
WebRTC学习(一)WebRTC了解
RTCPeerConnection类是整个WebRTC的一个核心类,它是上层的一个统一的接口,但是在底层做了非常多的复杂逻辑,包括了整个媒体的协商,流和轨道的处理,接收与发送,统计数据,都是由这一个类处理的。
所以对上层来说,你可能简单的调用了这个类或者里面的几个简单的API,但是实际在底层做了大量的工作,所以这个类是整个WebRTC的一个核心。
RTCPeerConnection基本格式:
注意:configuration配置参数,是可选的
(二)RTCPeerConnection方法分类
1.媒体协商
第一类,就是媒体协商相关的,比较简单,包括四个方法。
通过这几个方法之后,就可以拿到整个双方之间的媒体信息,然后双方会进行交换信息,去协商双方用的编码器,音频格式,协商一致后,他才真正地进行数据传输与编解码。
2.Stream/Track
第二大类,就是流与轨道,在整个WebRTC当中,每一路它是一路流,这个流里面有很多轨,有音频轨和视频轨,多路音频轨多路视频轨,在此前MediaStream中已经做过介绍,那在这里是同样的理念,在传输中我们传输的就是Stream和轨,在轨道中就包含了音频数据和视频数据。
3.传输的相关方法
第三类,是与传输相关的,就是通过RTP协议去传输,通过RTCP反馈这个链路的质量好坏。再通过数据的统计分析,查看链路的质量,是不是已经发生拥塞,还是说链路就是不太好的,都可以通过这个传输相关的方法来获取到去计算。
4.统计相关方法
最后一类的就是统计相关的,包括编解码器,音频格式,视频格式,整个传输相关的数据都可以通过这个统计相关的方法汇报。
以上就是RTCPeerConnection方法的分类。在后面,我们也会学习到如何通过这些方法来获取到这些数据,来为我们真正的产品的质量提供帮助。
(三)媒体协商过程(其中SDP信息和Candidate数据的处理,详细参考二:端对端连接)
对于这两个端来说,一个假设是A,第二个是B。
1.对于A,首先是要创建一个offer,这个offer就是SDP的一种描述格式去描述的,实际就形成了一个SDP。SDP是包含了一些媒体信息和编解码信息,包括这个传输的相关的信息。
2.创建完成SDP之后,A通过云端的信令Channel将SDP传给B,但是传之前的那他还要调一个方法setLocalDescription,触发一个非常重要的动作,就是setLocalDescription会触发一个非常重要动作就是去收集candidate,就是收集候选者。最后通过信令服务器发送SDP信息,在收到candidate后(由TURN/STUN服务器响应),也通过信令服务器转发给对端!!!
补充:收集候选者
在p2p学习过程中了解到,当我们去创建这个连接之前,首先要拿到所有的那种候选者,去收集候选者,是像这个stun或者是turn发送一个请求,在这个请求过程中,会拿到本地主机的IP地址,还有通过NAT之后反射的这个地址以及TURN服务的中继地址。
3.B端收到这个offer之后,他首先调用setRemoteDescription,将这个offer所形成的SDP的这个数据放到自己远端的描述信息的槽里。
4.之后B端要回一个answer,通过这个PeerConnection连接,调用方法去创建answer,也就是说创建一个包含B端的媒体信息(和A端offer中的信息一样)。
5.在传递answer到云端信令服务之前,B端也会去调用setLocalDescription方法,去触发收集候选者,我的这个网络有多少个候选者都要收集起来形成一个列表,调用这个函数之后它将请求获取候选者,之后通过这个信令服务就将answer转给了A,同时处理候选者列表。
6.A收到这个answer之后它又把它存到这个setRemoteDescription,存到它的远程槽里,这样在每一端实际都有两个SDP。
7.那么第一个SDP是我自己的这个媒体信息,第二个SDP是描述对方的媒体信息。那个拿着这两个媒体信息之后,他在内部就进行一个协商,协商双方所支持的音视频、编解码格式,取出这个交集之后,整个协商过程就算建立完成。
8.协商完成之后才能进行后面的操作,进行真正的编解码。传输数据到对方进行解码,去渲染音频,渲染视频。这就是完整的一个协商过程。
总之,每一端是有四个步骤:
调用方创建offer,设置LocalDescription,然后是接收answer,设置RemoteDescription;
被调用方,他就是先接收offer,然后设置setRemoteDescription,然后是创建answer上设置setLocalDescription。
(四)协商状态变化
那么下面呢,我们再看看协商的状态的变化:stable、have-local-offer、have-remote-offer、have-local-PRanswer、have-remote-PRanswer
1.当我们一开始创建这个RTCPeerConnection的时候,处于stable稳定状态,那么这个时候实际connection就可以使用了,但用的时候它是不能进行编解码的,为什么呢?
因为他没有进行数据协商,虽然我这个connection类是可以用,但是并没有进行数据协商,所以他没法儿进行数据的传输与编解码。
2.对于调用者来说,首先创建了connection之后,需要创建这个offer,创建offer之后通过调用那个setLocalDescription将这个offer传参进去后,状态变化,变成什么呢?
变成have-local-offer,当调用者设完这个之后,如果对方没有给我回他的answer的时候,那实际我的状态就一直处于have-local-offer状态,无论我再接受多少次这个setLocalDescription方法仍在处理这个状态,所以这个状态是不会变的。
3.那什么时候调用者才会进行编解码呢?只有在远端的answer回来的时候,如前面所讲的远端的answer创建好,然后通过消息传给这个调用者的时候,那它会调用这个setRemoteDescription,那么将answer设进去之后,他又回到了stable状态,这个时候RTCpeerConnection又可以使用了,并且是已经协商过的了,这时候调用端可以进行编解码,进行传输,这是对于调用者来说。
4.那么对于这个被调用者来说,同样,那当他收到这个offer之后呢,它要调用setRemote offer,这个时候呢,他从那个stable状态就变成了have-remote-offer,那同样的,当他自己创建了一个answer之后,并且调用了setLocalDescription这个方法将answer设置进去之后,他又从这个remote-offer变成了stable状态,那这个时候他也可以工作了。
以上状态变化过程如下:
对于调用者来说,首先在创建offer之后呢,会调用setLocalDescription将这个offer设置进去,调用者的状态,就变成了have-local-offer,那当他收到对端的这个answer之后呢,它会调用setRemoteDescription将这个offer设置进去,这样就完成了一个协商,就从这个have-local-offer变为了stable状态,那他就可以继续下面的工作了。 而对于被调用者,他首先是从信令服务器收到一个offer,那他首先调用setRemoteDescription获取这个offer,那它就变成了have-remote-offer状态,这个时候再调用自己的这个create answer方法, 创建完自己的这个answer之后调用setLocalDescription(answer)那就从这个have-remote-offer变为了stable状态,这样的被调用者他也就完成了自己的协商工作,可以继续下面的操作。
但是还是两种情况,会有一种中间的这个状态叫做PRanswer,就是提前应答,这个状态是什么时候会产生呢,就是在双方通讯的时候其中被调用者还没有准备好数据的时候,那可以先创建一个临时的这个answer。
那这个临时的answer有一个特点,就是它没有媒体数据也就是说没有音频流和视频流,并且将这个发送的方向设置成send only,
什么意思呢, 对于B来说,他回的这个answer是一个什么样的answer呢 ?
就是说,我的媒体流还没有准备好,所以就没有媒体流,但是我呢,只能发送,不能接受,当他发给对方A的时候,A收到这样一个send only,他就知道,对方还不能进入数据,所以这时候他们的通讯虽然是做了的协商,但是他们之间还不能进行通讯。
因为第一个是对方没有媒体流,第二个是对方不接受我的数据。
处于这样一个状态有什么好处呢?
那就是可以提前建立这个链路的连接,也就是说包括ICE,包括这个DLS这些跟链路相关的这个协商其实都已经创建好了,对刚才我们已经介绍了,就是对于B来说,他已经提前准备好了一个answer,但这个answer里有没有媒体数据,但是呢,实际是有网络数据的,我收集的各种各种候选者实际都已经有了。
那么就可以提前交给这个A,那AB之间,实际就是链路层已经协商好了,包括这个DLS还要进行这个握手,因为是安全加密,加密所以要进行握手,握手的时间其实还是蛮长的,那在B准备好这个自己的流之前,将所有的链路都准备好,
那一旦这个B向那个用户申请说想开启音频和视频,当用户授权说可以,这个时候呢,他们拿到数据之后,只要将数据传进去,就可以进行这个通讯了。
以上状态变化过程如下:
在B没有准备好之前,他可以使用一个PRanswer,就是提前预定好的一个answer给这个A发过去,发过去之后,它就变成了这个have-remote-offer这个状态,这是一个中间状态,在这个状态下,双方的这个链路是可以协商好的,只是没有这个媒体数据。 当B设置好他自己的媒体流之后,就是一切都准备好之后,然后再给他回一个最终的answer,当调用者收到它这个最终的answer之后呢,他又变成了stable状态,那双方就可以就真正协商好了。 这时候呢,实际是减少了底层的这个网络流的这个握手,以及一些其他的逻辑处理工作,这样就节省了时间。
对于对端A也是类似的,所以在他回这个真正的answer之前,他是处于这have-local-PRanswer的,当真正的这个最终的Answer,准备好之后,再重新设一下setLocalAnswer,他又变成了stable状态。
这就是一个整个协商完整的一个状态变化,只有在整个协商完成之后,才能进行我们后边的真正的音视频数据的传输以及编解码,这就是协商状态的变化。
(五)媒体协商方法
createOffer,创建一个本地的这个媒体信息,音频编解码视频编解码等等。
对于这个对端呢,就是收到offer之后呢,它会创建一个createAnswer,这是第二个方法,也就是说我本地的一个信息最终要传给这个调用者。
那第三个就是setLocalDescription,我把我自己本地的这个SDP的描述信息设置好之后我就可以触发去采集这个收集候选者了 。
那第四个就是setRemoteDescription,当收到到对端的这个描述信息之后,将它设的setRemoteDescription这个槽儿里去,在内部做真正的协商,就是媒体协商的方法。
createOffer:
PeerConnection类中有一个方法就是createOffer,它有一个可选的option,有几个选项,每个选项都有其特殊的意义。返回的是一个promise,创建成功之后有一个逻辑处理,失败会做另外一个逻辑处理。
createAnswer:
那这个createAnswer这个格式其实跟他是类似的,就变成了createAnswer,还有一个option,这个option其实就是作用不大,主要是createOffer是这个option有很多作用。
setLocalDescription:
setLocalDescription的格式就是将createOffer或者createAnswer的结果,包括参数设置到这里就设置好了,
setRemoteDescription:
同样道理,setRemoteDescription。那这个格式也比较简单,也是刚那个对端的这个sessionDescription设置进来就好了,这是四个协商相关的方法。
(六)Track方法
要在RTCPeerConnection里就有两个重要的这个Track的方法,一个是添加,一个是移除。
这个比较简单,添加的格式就是:
第一个是要添加的这个Track的是音频的Track还是视频的Track。
那么第二个是stream,那么这个stream从哪儿来呢?实际就是我们之前介绍的getUserMedia那里我们会拿到一个流,这个流里面可能有音频Track有视频Track,那就要遍历一下,让他们一个个都加入到这个PeerConnection里去,这样PeerConnection就可以控制这每一路轨了,从轨中获取到数据进行发送,这是添加。
还有其他一些参数。
removeTrack这个比较简单,就是将Addtrack里头这个send放进去,然后他就可以这个移除掉。
(七)重要事件
就是PeerConnection还有几个比较重要的事件,现在呢,我们首先介绍两个:
那么第一个是这个协商事件,当进行媒体协商的时候,就会触发这个事件,onnegotiationneeded就是需要协商,只要协商的时候会触发这个事件。
那么第二个是onicecandidate,就是当我们收到一个ICE的候选者的时候,也会从底层触发这个事件,告诉我们现在有一个候选者来了,那么我们要拿到这个候选者,将它添加到我们的这个ICE里去。
二:端到端连接的基本流程
端到端连接的一个基本流程,下图非常清楚的表达了A与B这两个端首先进行媒体协商、最终进行链路的连接、最后进行媒体数据的传输,下面就来进行分析:
(一)流程图成员
首先在这张图里面有4个实体:
第一个是A,也就是端到端连接的A端。
然后是B,是端到端连接的B端。
然后是信令服务器Signal。
最后是stun/turn,这个stun和turn服务用的是同一台服务器,既具有stun功能又具有turn功能。
(二)通信流程
1.首先A是发起端也就是呼叫端,呼叫端要与信令服务器建立连接,被呼叫端B端也要与信令服务器建立连接,这样他们就可以经过信令服务器对信令消息进行中转。
2.接下来A如果想要发起呼叫,首先它要创建一个PeerConnect,对端的连接对象,创建一个这样的实例,之后通过getUserMedia拿到本地的音视频流,将这个流添加到连接里去,这是第一步。
在进行媒体协商之前,我们需要先将流(本地采集的数据)添加到peerConnection连接中去。这样在媒体协商之前,我们才知道有哪些媒体数据。
如果先做媒体协商的话,知道这是连接中没有数据媒体流,就不会设置相关底层的接收器、发送器,即使后面设置了媒体流,传递给了peerConnection,他也不会进行媒体传输,所以我们要先添加流
3.接下来第二步它就可以调用PeerConnect的CreateOffer的方法去创建一个Offer的SDP,创建好SDP之后再调用setLocalDescription,把它设置到LocalDescription这个槽里去,那调用完这个方法之后实际在底层会发送一个bind请求给stun和turn服务,那这个时候它就开始收集所有与对方连接的候选者了。(还没收集完成,因为stun和turn服务还没有进行响应)
4.那与此同时调用完setLocalDescription之后,那之前CreateOffer方法拿到这个SDP那也要发送给信令服务器,那通过信令服务器的中转,最终转给B,这个时候B就拿到了offer,也就是说A这端的媒体相关的描述信息。
5.我们再来看B端 ,B端收到这个SDP之后呢,首先要创建一个PeerConnetion,创建一个连接对象,创建好这个对象之后它会调用setRemoteDescription将这个收到的SDP设置进去,那设置完成之后它要给一个应答,它要调用Create Answer,这时候它就产生了本机相关的媒体的信息也就是Answer SDP,创建好之后它也要调用setLocalDescription,讲这个本地的Answer SDP设置进去,这样对B来说它的协商就OK了。也就是说它有远端的SDP同时它自己这端的SDP也获取到了,这时候在底层就会进行协商。
6.对于B端,在setLocalDescription的时候它也要向stun和turn服务发送一个bind请求,也就是收集它能够与A进行通信的所有的候选者,在调用完setLocalDescription之后,它将它这个Answer SDP发送给信令服务器 ,通过信令服务器又转给了A,那A这个时候就拿到了B 这一端的媒体描述信息,然后它再设置setRemoteDescription,那这个时候A也可以进行媒体协商了,这个时候A和B进行媒体协商的动作就算完成了。这是媒体协商这一部分。
7.那接下来stun和turn服务将这个信息回给A,这个时候就会触发A端的这个onIceCandidate事件,因为我们上面是有一个请求(3中出现的),所有这个时候我们就能收到很多不同的onIceCandidate,A收到这个候选者之后它将这个候选者发送给这个信令服务器,通过信令服务器转给对端,也就是让对端知道我都有哪些通路(让B端知道本机A有哪些通路),那对端B收到这个Candidate之后要调用AddIceCandidate这个方法将它添加到对端的这个连接通路的候选者列表中去。
8.那同样的道理,当B收到这个Candidate之后,它也发给信令,通过信令转发给A,那这个时候A也拿到B的所有的候选者,并将它添加到这个候选者列表中去,也就是AddIceCandidate,那这个时候双方就拿到了所有的对方的可以互通的候选者,这个时候它底层就会做连接检测,看看哪些,首先他会做一个个的Candidate pair也就是候选者对,然后进行排序,排序完了之后进行连接检测等等等一系列的连接检测。
9.在我们之前都做过这个方面的介绍,当它找到一个最优的线路之后呢,A与B就进行通讯了,那首先是A将数据流发送给B,那B在收到这个数据流之后,因为它们前面已经做了绑定了,就知道是谁来的数据给我了,给我之后就与它的这个Connection进行对连,收到这个数据之后它是不能显示的,B虽然收到数据但是还是显示不出来,那它要将这个数据进行onAddStream,要添加进来,添加进行之后才能把这个视频数据和音频数据向上抛,那才能走到上一层进行视频的渲染和音频的渲染。
那以上就是一个基本的端对端连接的基本流程。那经过这样一个分析大家就会清楚,整个A要与B进行通讯,要走几步:
第一大块就是整个媒体的协商,看A端有什么媒体能力看B端有什么媒体能力,他们直接所有的媒体取一个交集,取大家都能够识别的支持的能力,包括音频编解码视频编解码,这个采样率是多少,帧率是多少,以及网络的一些信息;
第二大部分就是通过ICE对整个可连通的链路进行这个链路地址的收集,它收集完了之后进行排序和连接检测,找出双方可以连接的最优的这条线路;
那么最后拿到线路之后就可以进行媒体数据的传输了,当从一端传输到另一端之后呢,另一端会收到一个事件,就是onAddStream,当收到这个事件之后就可以将这个媒体流添加到自己的video标签和audio标签中进行音频的播放和视频的渲染,这个就是整个端对端连接的基本流程。
也就是分成这个三大块,熟悉了这个基本的流程之后,我们再编写这个程序的时候,就非常的简单了。
我们只要按照这个步骤一步步的 操作那么就能实现两个端之间的通讯。
三:实战音视频通信
为了尽量的简化,并没有使用真实的跨网络(从一台电脑的网络到另一台电脑的网络进行真实的音视频的传输),而是在我们一个页面里面其中一个video取展示我们本地采集的音频和视频,
那之后创建两个PeerConnection,然后将这个媒体流加入到其中一个PeerConnection之后让他们进行连接,连接之后进行本机底层的网络传输,传到另一端的PeerConnection,
当另一端PeerConnection收到这个音视频数据之后取回调这个事件,也就是onAddStream,当另一端收到这个onAddStream之后,将这个收到的数据转给视频标签,视频就被渲染出来了,
虽然没有经过真实的网络,但是他们整体的流程和真实网络的流程是一摸一样的,虽然不用信令传输了,但是我们还是要走这样一个逻辑。
那么在后面我们就会将真实的网络加入进去,那么大家就会看到实际整个流程并没有变,只是把这个信令通过信令服务器进行中转,网络连接也不是在自己本地中转 ,而是通过真实的网络进行传输。
(一)代码实现
'use strict' var http = require("http"); var https = require("https"); var fs = require("fs"); var express = require("express"); var serveIndex = require("serve-index"); var socketIo = require("socket.io"); //引入socket.io var log4js = require('log4js'); //开启日志 var logger = log4js.getLogger(); logger.level = 'info'; var app = express(); //实例化express app.use(serveIndex("./")); //设置首路径,url会直接去访问该目录下的文件 app.use(express.static("./")); //可以访问目录下的所有文件 //https server var options = { key : fs.readFileSync("./ca/learn.webrtc.com-key.pem"), //同步读取文件key cert: fs.readFileSync("./ca/learn.webrtc.com.pem"), //同步读取文件证书 }; var https_server = https.createServer(options,app); //绑定socket.io与https服务端 var io = socketIo.listen(https_server); //io是一个节点(站点),内部有多个房间 https_server.listen(443,"0.0.0.0"); //---------实现了两个服务,socket.io与https server;都是绑定在443,复用端口 //-----处理事件 io.sockets.on("connection",(socket)=>{ //处理客户端到达的socket //监听客户端加入、离开房间消息 socket.on("join",(room)=>{ socket.join(room); //客户端加入房间 //io.sockets指io下面的所有客户端 //如果是第一个客户端加入房间(原本房间不存在),则会创建一个新的房间 var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = Object.keys(myRoom.sockets).length; //获取所有用户数量 logger.info("the number of user in room is:"+users); //开始回复消息,包含两个数据房间和socket.id信息 //socket.emit("joined",room,socket.id); //给本人 //socket.to(room).emit("joined",room,socket.id); //给房间内其他所有人发消息 //io.in(room).emit("joined",room,socket.id); //给房间中所有人(包括自己)发送消息 socket.broadcast.emit("joined",room,socket.id); //给节点内其他所有人发消息 }); socket.on("leave",(room)=>{ var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = Object.keys(myRoom.sockets).length; //获取所有用户数量 logger.info("the number of user in room is:"+(users-1)); socket.leave(room); //离开房间 //开始回复消息,包含两个数据房间和socket.id信息 socket.broadcast.emit("leaved",room,socket.id); //给节点内其他所有人发消息 }); socket.on("message",(room,msg)=>{ var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 logger.info("send data is:"+msg); socket.broadcast.emit("message",room,socket.id,msg); //给节点内其他所有人发消息 }); });
服务器实现
<html> <head> <title> WebRTC PeerConnection </title> </head> <body> <h1>Index.html</h1> <div> <video autoplay playsinline id="localvideo"></video> <video autoplay playsinline id="remotevideo"></video> </div> <div> <button id="start">Start</button> <!--采集音视频数据--> <button id="call">Call</button> <!--创建双方的peerconnection,开始通信--> <button id="hangup">HangUp</button> <!--挂断--> </div> </body> <script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script type="text/javascript" src="./js/main.js"></script> </html>
index.html
main.js主要javascript文件:
'use strict'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");var localStream; //设置全局流,用来addStream发送给对端时使用var pc1; //处理pc1与pc2时候,只需要站在一个角度就可以了,因为对端也是一样的var pc2;function handleError(err){ console.err(err.name+":"+err.message);}function getMediaStream(stream){ localVideo.srcObject = stream; //显示到网页视频控件 localStream = stream; //保存到全局流中}//采集本机音视频数据function start(){ if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ console.error("the getUserMedia is not support!"); return; }else{ var constraints = { audio:false, video:true }; navigator.mediaDevices.getUserMedia(constraints) .then(getMediaStream) //获取数据流 .catch(handleError); }}function getRemoteStream(e){ //会有多个流 remoteVideo.srcObject = e.streams[0]; //只取其中一个就可以了,就将远端的音视频流传给了remoteVideo}function getAnswer(desc){ pc2.setLocalDescription(desc); //7.远端设置本地描述信息 //发送描述信息SDP到signal信令服务端,与pc1进行交换 //8.pc1设置远端描述信息 pc1.setRemoteDescription(desc); //-----这里开始获取了所有对端的SDP信息,双端信息协商完成!!!!----}function getOffer(desc){ //获取了描述信息,开始设置到peerConnection中去 //4.设置本地的描述信息,添加到peerconnection pc1.setLocalDescription(desc); //发送描述信息SDP到signal信令服务端,与pc2进行交换 //5.对端接收设置SDP信息 pc2.setRemoteDescription(desc); //6.创建Answer信息 pc2.createAnswer() .then(getAnswer) //7.远端设置本地描述信息 .catch(handleError);}function call(){ //1.创建peerConnect,pc1与pc2同时连接到signal服务器(这里是一起到本机) /* 在这个Connection里面实际上是有一个可选参数的, 这个可选参数就涉及到网络传输的一些配置 我们整个ICE的一个配置,但是由于是我们在本机内进行传输,所以在这里我们就不设置参数了,因为它也是可选的 所以它这里就会使用本机host类型的candidate */ pc1 = new RTCPeerConnection(); //调用方 pc2 = new RTCPeerConnection(); //被调用方 //当收到candidate后,会触发事件,获取候选者列表,之后调用send candidate发送给signal服务器,从而发送给对端。双方获取之后进行连通性检测 pc1.onicecandidate = (e) => { pc2.addIceCandidate(e.candidate); //开始添加给对端 }; pc2.onicecandidate = (e) => { pc1.addIceCandidate(e.candidate); //开始添加给对端 }; //pc2是相对特殊的,因为是被调用者,用于接受数据 pc2.ontrack = getRemoteStream; //被调用方,接收数据,有数据经过的时候调用ontrack事件 //下面要先添加媒体流,然后才进行媒体协商 //2.添加媒体流 localStream.getTracks().forEach((track)=>{ //获取所有的轨 pc1.addTrack(track,localStream); //将本地产生的音视频流添加到pc1的peerConnection }); //3.创建offer var offerOptions = { offerToRecieveAudio:0, //不处理音频 offerToRecieveVideo:1 }; pc1.createOffer(offerOptions) .then(getOffer) //4.设置本地的描述信息,添加到peerconnection .catch(handleError);}function hangup(){ pc1.close(); pc2.close(); pc1 = null; pc2 = null;}btnStart.onclick = start;btnCall.onclick = call;btnHangup.onclick = hangup;
(二)测试结果
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/34696.html