1、引入

implementation 'org.webrtc:google-webrtc:1.0.24465'// 参考:https://webrtc.org/native-code/android/

本文有部分内容参考自即时通讯网(http://www.52im.net/thread-265-1-1.html)

2、WebRTC介绍

WebRTC,网页实时通信(Web Real-Time Communication)的缩写,它是一种支持跨平台的实时语音通讯、视频通讯的技术。

WebRTC的音视频通讯技术是基于p2p实现的,这种技术需要处理两个主要的问题:

  • 信令交互,本文使用WebSocket来实现,主要有以下几个功能:
    • 控制通信开启或关闭
    • 告知通讯双方媒体流数据,比如解码器、媒体类型等
    • 交换通讯双方的IP地址、端口号
    • 发生错误时告知彼此
  • P2P通信

    现实网络中主要有三种环境:

    1. 公共网络:这类网络主机之间可以不受限制地进行直接访问,现实中这种网络很少存在。
    2. NAT网络:这类网络中主机位于私有网络中,没有单独的公有IP,访问私有网络中的主机需要打洞,通过打洞我们可以发现主机在公网中的IP,STUN协议就是用来帮助实现这一功能的。
    3. 严格受限的NAT网络(对称NAT):这类网络中的主机位于私有网络中,只能单向访问外网。对称NAT网络特性是当目的地址的IP或者端口有一个发生改变时,NAT都会重新分配一个端口使用,所以这个时候我们不能选择使用P2P来实现网络通信,可以借助公共网络上的TURN服务器来实现数据传输,TURN协议就是解决此类问题的。

2.1 P2P实现的原理:

首先介绍一些基本概念,NAT(Network Address Translators),网络地址转换。网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的是为了地址重用。NAT主要分为两类:基本的NAT和NAPT(Network Address/Port Translators)

最先提出的是NAT,由于在子网络中只有很少的节点需要与外网连接,那么这个子网络中其实只有少数节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。因此,基本的NAT实现的功能很简单,当子网中的节点需要访问外网时,NAT负责将子网的IP转换为全球唯一的IP地址并发送出去(只会改变IP包中的原IP地址,不会改变端口号)。另外一种NAT叫做NAPT,这个是现在普遍使用的NAT,它会改变IP包中的原IP地址,同时也会修改原IP包中的端口号。

举个例子:

有一个私有网络中的主机A(IP:10.0.0.10,port:1234)想访问外网主机B(IP:18.181.0.31,port:1235),那么数据包通过NAT(假设外网IP为155.99.25.11,端口为6000)时会发生什么呢?

NAT会改变这个数据包的原地址和端口号,改为155.99.25.11,端口改为6000。NAT会记住6000端口对应的是(IP:10.0.0.10,port:1234),以后从外网主机B(IP:18.181.0.31,port:1235)发来的数据NAT会自动转发给主机A(IP:10.0.0.10,port:1234),其他的IP发送过来的数据将被NAT丢弃。

接上面的例子,如果主机A又向另外一个主机C发送了一个UDP包,那么这个UDP包在通过NAT时会发生什么呢?

这时可能会发生两种情况:

  • 重新为主机A分配一个端口号,比如6001,这种就属于对称NAT,会导致很多P2P软件失灵
  • 继续使用之前的端口号6000,这种叫锥形NAT(Cone NAT)。

    Cone NAT又分为3种:

    • 全克隆(Full Cone):NAT把所有来自相同内部主机的IP地址和端口的请求映射到相同的外部IP地址和端口,任何一个外部主机均可通过该映射访问内部该IP地址和端口对应的主机。
    • 限制性克隆(Restricted Cone):在全克隆的基础上多了IP的限制。外部主机能访问内部主机的条件是内部主机发送过IP包给外部IP地址为X的主机,这时外部IP地址为X的主机才能访问该内部主机。
    • 端口限制性克隆(Port Restricted Cone):在限制性克隆的基础上多了端口的限制。外部主机能访问内部主机的条件是内部主机发送过IP包给外部IP地址为X、端口为Y的主机,这时外部IP地址为X、端口为Y的主机才能访问该内部主机。

通过以上的信息我们可以知道,在有NAT存在的情况下,子网内的主机可以直接连接外部的主机,而外部的主机想访问内部的主机就比较困难了(这正是P2P需要的)。那么如果外部主机想访问内部主机该怎么做呢?

我们必须在NAT上打一个“洞”,这个洞只能由内部主机来打。而且这个洞是有方向的,比如从内部某台主机(192.168.0.10)向外部的某台主机(219.60.0.37)发送一个UDP包,这个时候我们就打了一个方向为219.60.0.37的洞,以后219.60.0.37可以通过这个“洞”来与内部主机(192.168.0.10)进行通信,但是其他的IP传过来的数据则会被直接丢弃。

2.2 P2P的常用实现

2.2.1 普通的直连式P2P实现

通过上面的理论,实现两个内网主机通讯就只差最后一步了:两边都无法主动发连接请求,那我们如何打这个洞呢?我们需要一个中间人来联系这两个主机。

举个例子:

Client A想访问Client B,首先,Client A登录服务器,NAT A为Client A分配了端口6000,Server S收到了Client A的地址为:202.187.43.2:6000,这就是A在公网中的地址。同样,Client B也登录Server S,NAT B为Client B分配了端口4000,Server S收到B的地址为:202.187.43.3:4000,,这就是B在公网中的地址。

此时,Client A可以通过Server S获取到B的地址202.187.43.3:4000,如果A直接向B发送数据包,这个数据包会被NAT B认为是不安全的,直接丢弃这个数据包。现在我们需要在NAT B上打一个方向为202.187.43.2:6000(即Client A的IP地址)的洞,Client A发送的数据包B就能收到了。那么这个打洞的命令由谁来发呢?自然是Server S。

总结一下这个过程:Client A想向Client B发送消息,那么Client A可以先向Server S发送命令,请求Server S命令Client B向Client A方向打洞,然后ClientA就可以通过Client B的外网地址与Client进行通信了。

注意,上面这个过程只适用于Cone NAT,如果是严格受限的NAT网络,当B向A打洞时,A的端口已经重新分配了,Client B无法知道这个端口,这时候P2P传输会失败。

2.2.2 STUN方式的P2P实现

   STUN方式的探测过程需要有一个有公网IP的STUN Server,位于NAT后面的主机需要与STUN Server发送多个UDP数据包,UDP数据包中需要包含NAT外网IP、Port等信息。主机通过是否得到这个UDP包和包中的数据来判断NAT的类型。假设有主机B和Server S,Server C有两个IP:IPC1和IPC2,下面是NAT的探测过程:

  1. 判断是否在公网中:B向C的IPC1和port1发送一个UDP包,C收到后,它会把收到的源IP和端口写进UDP包中,并通过IPC1和port1发还给B,B收到该UDP包后,将IP地址与本地的IP地址对比,如果一样,则处在公网中,否则进行step2
  2. 检测NAT的类型:B向C的IPC1和port1发送一个UDP包,请求C用另外一个IP地址IPC2和port2给B返还一个UDP包。B如果能收到该UDP包,说明是full cone NAT,如果没收到,进行step3
  3. B向C的IPC2和port2发送一个UDP包,C收到后将源IP和port写入UDP包返还给B,B收到后,跟step1中的端口号进行对比,如果不一致,说明是对称NAT,原理很简单,根据对称NAT的规则,目的地址的IP和port有一个改变的时候,都会导致原地址的port重分配,此时就应该放弃p2p了。如果一致,进行step4
  4. B向C的IPC2发送一个UDP包,请求C使用另一个端口port返还数据。如果B能收到C传回的数据,说明只要IP一致,端口不一样也能收到数据,这就是restrict CONE NAT,否则是port restrict CONE NAT

3、ICE框架

  基于信令协议的多媒体传输是一个两段式传输,首先通过信令协议(如WebSocket)建立一个连接,通过该连接,双方交换传输媒体时所必须的信息。
  基于传输效率的考虑,在完成第一阶段的交互之后,通信双方会建立一条通道来实现媒体传输,以减少传输时延、降低丢包率并减少开销。由于使用新的链路,当传输双方有任意一方位于NAT之后,新的传输链路就需要考虑NAT穿越的问题了
  通常有四种形式的NAT,每一种NAT都有相应的解决方案,然而在复杂的网络环境中,由于每一种NAT穿越方案都局限于对应的NAT方式,这些方案就给整个系统带来了一定的复杂性。在这种背景下,交互式连通建立方式(Interactive Connectivity Establishment)即ICE解决方案应运而生,ICE能够在不增加整个系统的复杂性和脆弱性的情况下,实现对各种形式的NAT的穿越。

ICE工作的核心

3.1 收集地址

Android WebRTC使用解析_第1张图片
  其中派生地质是指:通过本地地址向STUN服务器发送STUN请求后获得的网络地址
  服务器反向地址:终端经过一层或多层NAT穿透后,在STUN服务器上收到的经过NAT转换后的地址
  中继地址:STUN服务器收到STUN请求后,在本地分配的一个代理地址。所有被路由到该代理地址的网络包都会被转发到服务器反向地址,继而穿透NAT发送到终端。

3.2 连通性检查

  主机A收集到所有的候选地址后,对其按优先级进行排序,再将其作为SDP的属性通过信令信道发送给主机B。主机B收到主机A的候选地址后,执行相应的过程,将主机B的候选地址发送给主机A。这样主机A和主机B都拥有了一个完整的地址对,然后准备执行连通性检查。

执行连通性检查的原理:

  • 按照优先顺序对地址对排序
  • 逐个地址对去发送检查包
  • 收到另一个主机的确认检查包

  首先,主机A将本地候选地址对与远程地址对进行配对,比如本地有n个地址,远程有m个地址,则可以组成n×m个地址对。对这些地址对进行连通性检查是通过发送和接收STUN请求完成的。若通信双方以某一地址对完成一次四次握手过程,那么该地址对就是有效地址对。
  四次握手:通过一对地址对中的本地地址给远程地址发送一个STUN请求,如果能成功收到响应,则称该地址对是可接收的;当地址对中的本地地址收到远程地址的一个STUN请求,并成功的响应,则称改地址是可发送的;如果一个地址对是可接收的,同时又是可发送的,则称该地址对是有效的,可以用于媒体传输。

3.3 对候选地址对进行排序
  • 主机为每一个候选地址设置一个优先级,这个优先级连同候选地址对一起发送给远程主机
  • 综合本地地址和远程地址的优先级,计算出每一个地址对的优先级,这样,双方同一个地址对的优先级相同
3.4 进行SDP编码

  为了实现基于ICE的NAT穿越,增加了四个属性:

  • candidate属性:提供多种可能地址中的一个。这个地址是使用STUN连通性检查有效的地址。
  • remote-candidates属性:提供请求者想要应答者在应答中使用的远程候选传输地址标识。
  • ice-pwd属性:提供用于保护STUN连通性检查的密码。
  • ice-ufrag属性:提供用于在STUN连通性检查中组成用户名的片段。

4、WebRTC流程

  下面我们介绍一个使用WebRTC实现音视频通话的例子,由于这个例子来自于我现在开发的项目,为避免代码量太大不便于理清流程,有一些优化配置相关的代码便做了一些删改:

4.1 主叫方流程

1、 初始化WebSocket作为我们的信令服务器,创建PeerConnection实例

    /**     *  WebSocket     * @param url WebSocket对应的url     * @param listeners 这是对信令服务器WebSocket的监听     * @return     */    private IWsMgr obtainSignalWsMgr(final String url, final Collection listeners) {        JavaWsMgr wsMgr = JavaWsMgr.obtainWsMgr(url);//这里是根据url产生一个WebSocket实例        wsMgr.setWsStatusListener(new WsStatusListenerAdapter() {            @Override            public void onOpen(ServerHandshake serverHandshake) {                 ...            }            @Override            public void onMessage(String s) {                ...                PhoneWsBean phoneWsBean = parseObject(s, new TypeReference>();                ...                // 处理信令事件                handleSignalEvent(url, s, phoneWsBean, listeners);            }            @Override            public void onClose(int code, String reason, boolean remote) {               ...            }            @Override            public void onError(Exception e) {                ...            }        });        return wsMgr;    }

这个方法里的handleSignalEvent就是在处理信令事件,这个方法最终会调用ackIceServers

当收到服务器端发来的ackIceServers时,开始初始化PeerConnection:

    @Override    public void ackIceServers(final ArrayList iceServers) {        ...            //重点看这个方法            initPeer();        ...    }    private void initPeer() {        ...        // 这里创建PeerConnectionFactory        mPeerClient.createPeerConnectionFactory(this, mParameters, mIceServerParameters, mEvents);        ...        // 这里创建PeerConnection        mPeerClient.createPeerConnection(mIsVideo ? mRootEglBase.getEglBaseContext() : null, sv_localView, sv_remoteView, mIsVideo, isActive);    }   

我们看下PeerClient的createPeerConnection方法

    public void createPeerConnection(            final EglBase.Context renderEGLContext, final VideoRenderer.Callbacks mLocalRender, final VideoRenderer.Callbacks mRemoteRender, final boolean isVideo, final boolean isCall ){        ...        LinkedList ret = new LinkedList<>();        // 配置STUN服务器        for (int i = 0; i < mIceServerParameters.list.size(); i++) {            IceServersBean bean = mIceServerParameters.list.get(i);            String username = bean.getUsername();            String credential = bean.getCredential();            username = username == null ? "" : username;            credential = credential == null ? "" : credential;            ret.add(new PeerConnection.IceServer(bean.getUrl(), username, credential));        }        PeerConnection.RTCConfiguration rtcConfig =                new PeerConnection.RTCConfiguration(ret);        // 这里做一些配置工作        rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED;        ...//省略一些配置        rtcConfig.keyType = PeerConnection.KeyType.ECDSA;        // 用之前实例化好的PeerConnectionFactory创建PeerConnection,mPcObserver是一个监听        mPeerConnection = mFactory.createPeerConnection(                rtcConfig, createPcConstraints(), mPcObserver);        // 创建DataChannel,用于传输媒体数据        mDataChannel = mPeerConnection.createDataChannel(DATA_CHANNEL_TAG, init);        // 注册DataChannel监听        mDataChannel.registerObserver(new DcObserver());    }    /**     *DataChannel监听     */    public class DcObserver implements DataChannel.Observer {        @Override        public void onBufferedAmountChange(long l) {        }        @Override        public void onStateChange() {            switch (mDataChannel.state()) {            ...                case OPEN:                    // 创建好DataChannel之后,被叫方创建answer                    performAddStream();                    mPeerConnection.createAnswer(mPassiveAnswerObserver, createSdpConstraint());                    break;            }        }        @Override        public void onMessage(DataChannel.Buffer buffer) {            ...            // 解析buffer,当收到被叫的Answer时,通话建立成功            handlerMessage(stringDataChannelBean);        }    }

2、 初始化PeerConnection之后,创建本地流,必须添加这个stream才能对方才能听到音频或者视频

    private void performAddStream() {        if (mPeerConnection != null) {//这个步骤就是建立了通道就发出去;            mMediaStream = mFactory.createLocalMediaStream("ARDAMS");            // Create audio constraints.            mLocalAudioTrack = createAudioTrack();            mMediaStream.addTrack(mLocalAudioTrack);            mPeerConnection.addStream(mMediaStream);        }    }

3、 创建offer发给服务器

mPeerConnection.createOffer(mActiveCreateOfferObserver, createSdpConstraint());

mActiveCreateOfferObserver是一个监听

    private final SDPObserver mActiveCreateOfferObserver = new SDPObserver() {        private SessionDescription mSdp;        @Override        public void onCreateSuccess(final SessionDescription origSdp) {            mExecutor.execute(new Runnable() {                @Override                public void run() {                    String sdpDescription = origSdp.description;                    ...                    final SessionDescription sdp = new SessionDescription(                            origSdp.type, sdpDescription);                    //将会话描述设置在本地,                    mPeerConnection.setLocalDescription(mActiveCreateOfferObserver, mSdp);                    //接下来使用之前的WebSocket实例将offer发送给服务器,代码很简单就不贴上来了                }            });        }        @Override        public void onSetSuccess() {            ...        }    };

4、 收到对方回复的preAnswer,设置会话描述

    @Override    public void preAnswer(String fr, String frSid, String to, String toSid, final SessionDescription sdp) {        //主叫收到 preanswer        mPeerClient.setRemoteDescription(sdp, new PeerConnectionClient.SDPObserver() {            @Override            public void onCreateSuccess(SessionDescription sessionDescription) {            }            @Override            public void onSetSuccess() {            }        });            }

以上就是打电话过程中主叫的一个完整WebRTC流程,接下来我们看下被叫的WebRTC流程。

4.2 被叫方WebRTC流程

1、 首先也是先要初始化WebSocket与PeerConnection,这个步骤跟主叫方一致
2、 第二步开始有一些区别,被叫方收到主叫方发过来的数据包后,会在本地设置会话描述并且创建preAnswer

private void passiveSetRemoteAndCreatePreAnswer() {    mPeerClient.setRemoteDescription(mSdp, new PeerConnectionClient.SDPObserver() {        @Override        public void onCreateSuccess(SessionDescription sessionDescription) {        }        @Override        public void onSetSuccess() {            // 这里创建应答preAnswer            mPeerClient.passiveCreatePreAnswer();        }    });}/** * 被叫方 创建 pre answer */public void passiveCreatePreAnswer() {    ...    // mPreAnswerObserver是一个监听    mPeerConnection.createAnswer(mPreAnswerObserver, createSdpConstraint());    ...}/** * 被叫方 创建 preanswer的观察者 */private final SDPObserver mPreAnswerObserver = new SDPObserver() {   @Override   public void onCreateSuccess(final SessionDescription origSdp) {        String sdpDescription = origSdp.description;        ...        final SessionDescription sdp = new SessionDescription(        SessionDescription.Type.PRANSWER, sdpDescription); //新流程,被叫方收到 主叫方的offer以后 创建 pranswer        // 设置本地会话描述         mPeerConnection.setLocalDescription(mPreAnswerObserver, mSdp);         // 使用WebSocket发送answer给主叫方         sendPreAnswer(sdp);        }        @Override        public void onSetSuccess() {            ...        }}

3、 被叫点击接听电话以后,创建answer,到这一步就可以成功通话了

    public void passiveTryCreateAnswer() {        // 添加媒体流        performAddStream();        // 创建应答answer,mPassiveAnswerObserver是创建过程的一个监听        mPeerConnection.createAnswer(mPassiveAnswerObserver, createSdpConstraint());    }    /**     * 被叫方创建 media sdp answer 的 监听     */    private final SDPObserver mPassiveAnswerObserver = new SDPObserver() {        @Override        public void onCreateSuccess(final SessionDescription origSdp) {            String sdpDescription = origSdp.description;            final SessionDescription sdp = new SessionDescription(                    origSdp.type, sdpDescription);            // 设置本地会话描述            mPeerConnection.setLocalDescription(mPassiveAnswerObserver, mSdp);            ...        }        @Override        public void onSetSuccess() {            ...        }    };

总结

到此,通话连接过程就算正式完成了。接下来我们总结一下通话的整个流程:

主叫方:
  1. 初始化WebSocket作为我们的信令服务器,监听WebSocket与服务器的交互过程
  2. 初始化PeerConnectionFactory,使用PeerConnectionFactory得到PeerConnection实例(mFactory.createPeerConnection()),创建DataChannel,注册DataChannel监听。
  3. 添加媒体流(mPeerConnection.addStream()
  4. 创建offer并发送,创建成功后设置本地会话描述。
  5. 收到被叫方的preAnswer,设置对方的会话描述到本地(mPeerClient.setRemoteDescription()
  6. 被叫方点击接听按钮后,会创建一个answer发给主叫方,收到这个answer后设置本地会话描述,通话就建立成功了
被叫方
  1. 开始两步跟主叫方一致,初始化WebSocket与PeerConnection
  2. 将主叫方会话描述设置到本地(mPeerClient.setRemoteDescription()),设置完成后发送preAnswer给主叫方
  3. 被叫方点击接听按钮以后会添加媒体流(mPeerConnection.addStream())同时创建一个应答answer发送给主叫方,到此通话连接建立成功。

本文有部分内容参考自即时通讯网(http://www.52im.net/thread-265-1-1.html)

更多相关文章

  1. android 6.0及以下获取wifi mac地址
  2. Android下如何获取Mac地址
  3. 取WiFi MAC地址
  4. 为usb网卡设置ip地址之一
  5. android_m2repository_rxx.zip下载地址以及MD5
  6. Android studio 1.0.2 下载地址
  7. Android获取IP地址
  8. Android 获得本机ip地址和MAC地址
  9. Android获取当前网络状态和获取当前设备网络ip地址

随机推荐

  1. NDK集成libjpeg和libpng
  2. 进程与进程间的通信(一)——Aidl
  3. Android(安卓)Color类
  4. Innotrends 宣布基于 Android(安卓)的 Ca
  5. 持久化修改Android模拟器的system分区
  6. Android(安卓)UnitTest
  7. Android(安卓)NDK开发环境搭建
  8. 解決 android studio更新失败的一种情况
  9. [置顶] android性能测试工具之dumpstate
  10. [Unity3D]Unity3D游戏开发之Unity与Andro