欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

(安卓)WebRtc 实现音频和视频通话、屏幕共享的详细过程(包括转服务器设置)

最编程 2024-03-08 08:11:57
...

1.参考文献资料

  1. WebRtc所需服务器搭建流程
  2. WebRtc所需服务器搭建流程备用参考地址 2
  3. 开启WebRTC的一些“试用特性” (FieldTrials)
  4. WebRtc官网
  5. turn服务器Git仓库
  6. 官方WebRtc安卓端参考Demo
  7. 在WebRTC中如何控制传输速率呢?
  8. 关于H.264的码率,720P、1080P输出比特率设置
  9. webRtc打洞:p2p链接建立过程
  10. webrtc M66 华为手机h264硬编解码不支持问题

2.技术实现方案详情

WebRtc介绍

  • WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
  • WebRtc起初是由Global IP公司创造出来的,后来被Google收购并将这门技术改名为WebRtc。
  • WebRTC是一项实时通信领域革命性的技术,在实时音视频通信领域已经产生了深远的影响。
  • WebRtc主要被用于音视频即时会议通话。它不仅仅只支持Web端,它还支持Android、Ios、Pc等平台。现有的音视频通话、音视频多人会议都离不开WebRtc的使用。
  • WebRtc SDK内集成了音频的回音消除、噪音一致、自动增益控制、高通滤波器等功能。

在webrtc出现之前,音视频通话是通过服务器来进行转发的。
存在以下问题:

1、延时太高,因为多了一层的转发操作

2、服务器承载有限

WebRtc的出现则很有效的解决了这些问题的。

WebRtc多方通信架构:

WebRTC应用——无论是Web端还是Native端——通常都会有3种运行模式:P2P(Peer to Peer)模式,SFU(Selective Forwarding Unit)模式和MCU(Multi-point Control Unit)模式。

  • Mesh 方案,即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,每个客户端都和当前网络里的其他两个客户端均建立了独立的链接。
  • MCU(Multipoint Conferencing Unit)方案,该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
  • SFU(Selective Forwarding Unit)方案,该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。
    MCU和SFU都是经过一个中转服务器来转发数据:

在WebRtc中,两两客户端之间会有一个peer链接,里面可以推送MediaStream流,可以往MediaStream中添加视频轨、音频轨。如两个客户端之间各自在本地配置了一条视频轨和一条音频轨。那么每个客户端都会有四个通道。分别是:视频流下发、视频流上行、音频流上行、音频流下发。

WebRtc基本组成

Turn服务器

turn服务器的作用是用于打洞,也叫网络net地址穿透。可以直接使用官方提供的服务器包部署使用即可。不需要自己手写代码。
大致规则如下:需要建立链接的两个客户端,他们都必须能连接上我们的turn服务器(因为这两个客户端可以访问我们的turn服务器,那么就说明他们两个客户端是一定能够通讯的),turn服务器通过比对两个客户端的网络地址,给需要建立链接的两个客户端计算出最短的网络链接路径(即绕过尽可能少的路由器)。

网络穿透的大致原理则是:两个客户端通过服务器交换了对方的路由地址后,各自主动给对方的路由发送一条信息,这样可以使客户端当前的路由记录下目标的路由地址到路由表中,记录以后,对方的客户端给当前客户端发送信息时,信息就不会被客户端的父路由器给拒收掉。

房间服务器

这部分需要自己手写代码,部署一个房间服务器,所有客户端都与该服务器建立socket链接。根据客户端与服务器之间定义的协议来为房间内的客户端进行一个sdp的交换工作

集成了WebRtc SDK的客户端

  1. 客户端负责集成WebRtc SDK,使用SDK的Api给WebRtc配置我们的Turn服务器。
    与房间服务器建立长链接,借助房间服务器来给其他加入通话房间的设备交换SDP、ICE后,即可完成p2p链接的建立

PS:房间服务器和turn服务只负责引导客户端建立p2p链接,链接建议完成后,只要两个设备还处于一个局域网中,即使断开于turn服务、房间服务器的链接,通信也依然能够建立。

WebRtc Android端接入流程

1、引入WebRtc依赖库(也可以自己去拿WebRtc源码来进行编译AAR包,编译极慢,要好几天)

/** 引入webRtc **/
implementation 'org.webrtc:google-webrtc:1.0.+'
/** 引入webSocket,用于链接房间服务器 **/
api 'org.java-websocket:Java-WebSocket:1.3.9'

2、创建WebSocketManager,负责建立与房间服务器的链接(这部分的实现是与后端一起定义的,实现会不太一样,不贴代码了),借助房间服务器来交换SDP、ICE。

3、创建PeerConnectionManager类,此类负责的则是WebRtc相关的通讯协议建立,这部分详细讲述:

3.1、在此类初始化时,进行turn服务器地址的配置。

//定义一个存储IceServer的集合
private ArrayList<PeerConnection.IceServer> mICEServers;
//我们的turn服务器的地址   turn:IP?transport=udp
String turnServerIp="turn:"+Config.ServerIP+"?transport=udp";
PeerConnection.IceServer iceServer = PeerConnection.IceServer
                                        .builder(turnServerIp)
                                        .setUsername("admin")
                                        .setPassword("123456")
                                        .createIceServer();
mICEServers.add(iceServer);


//在Peer链接创建时会将上面存储的turn服务器地址添加进WebRtc的配置信息中
private PeerConnection createPeerConnection(){
    if (mPeerConnectionFactory == null) {
        mPeerConnectionFactory=createConnectionFactory();
    }
    //创建PeerConnection	mICEServers 服务器可以有多个,如果第一个失败则会找下一个
    PeerConnection.RTCConfiguration rtcConfiguration = 
        new PeerConnection.RTCConfiguration(mICEServers);
    return mPeerConnectionFactory.createPeerConnection(rtcConfiguration,this);
}

3.2、创建PeerConnectionFactory(通常只需要一个PeerConnectionFactory,借助PeerConnectionFactory来创建每个要链接的终端的链接Peer)

private PeerConnectionFactory createConnectionFactory() {
    /**
* 对PeerConnectionFactory 进行全局初始化
*/
    PeerConnectionFactory.initialize(PeerConnectionFactory
                                     .InitializationOptions
                                     .builder(mActivity.getApplicationContext())
        .setFieldTrials("WebRTC-H264Simulcast/Enabled/")
        .createInitializationOptions());
    //编码 分为  音频编码 和 视频编码
    //解码 分为  音频解码 和 视频解码
    VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), true, true);
    VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
    PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
    PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder()
        .setOptions(options)
        //              .setAudioDeviceModule(JavaAudioDeviceModule.builder(mActivity).createAudioDeviceModule())
        .setVideoDecoderFactory(videoDecoderFactory)
        .setVideoEncoderFactory(videoEncoderFactory)
        .createPeerConnectionFactory();

    return peerConnectionFactory;
}
  • PeerConnectionFactory.initialize()用于WebRtc的全局初始化
  • .setFieldTrials("WebRTC-H264Simulcast/Enabled/") 为配置WebRtc的实验特性,当前配置的"WebRTC-H264Simulcast/Enabled/"意为启用 H264Simulcast 特性,该特性效果为,在推流给多个设备时,编码端会编码多个不同码率、质量的视频码流,根据对方设备的性能、网络来发送对应质量的画面。即:编码端编码多种不同画面质量的h264视频流,WebRtc会根据接收端的性能、网络状况来发送对应画面质量的码流。以此来提高视频实时性。减少设备性能差距导致的视频帧解析耗时不一致问题。
  • WebRtc的mRootEglBase创建:mRootEglBase=EglBase.create();
  • new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), true, true);传入的参数依顺序是:WebRtc的全局上下文(与我们android的上下文不是同一个)、是否启用VP8编码支持、是否启用H264编码支持。

3.3、创建本地的流

mMediaStream是我们本地生成的总流,里面有多个轨道,如音频轨道、视频轨道。
通过调用addTrack将我们的音视频轨道添加进去总流中

private void createLoaclStream() {
    //添加一个总流
    mMediaStream=mPeerConnectionFactory.createLocalMediaStream("ARDAMS");//需要与后面的ARDAMSa0前缀一致
    //        MediaConstraints audioConstraints = createAudioConstraints();
    //        AudioSource audioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
    //        //音频是数据源.创建一个音频轨道
    //        /**
    //         * id规则   ARDAMS + a/v + 轨道号                a:audio音频   v:video视频
    //         */
    //        AudioTrack audioTrack = mPeerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
    //        mMediaStream.addTrack(audioTrack);
    //        //音频轨道创建成功 添加音频轨道的数据源

    if (videoEnable){
        //录屏的
        //摄像头分前置/后置;camera1/camera2
        //videoCapturer   数据源
        createVideoCapturer("screencast");
    }
}

3.4、创建音频轨道,并加入总流:

//    googEchoCancellation   回音消除
public static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
//    googNoiseSuppression   噪声抑制
public static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";
//    googAutoGainControl    自动增益控制
public static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl";
//    googHighpassFilter     高通滤波器
public static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
//音频轨道的功能开关
public MediaConstraints createAudioConstraints(){
    //类似于一个hashmap
    MediaConstraints audioConstraints = new MediaConstraints();
    audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_ECHO_CANCELLATION_CONSTRAINT,"true"));
    audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_NOISE_SUPPRESSION_CONSTRAINT,"true"));
    audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT,"false"));
    audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_HIGH_PASS_FILTER_CONSTRAINT,"true"));
    return audioConstraints;
}

MediaConstraints存储的是WebRtc中定义的一些功能、规则

通过mPeerConnectionFactory.createAudioSource来创建AudioSource
再通过创建的mPeerConnectionFactory,来创建我们的音频轨

之后将音频轨加入到我们本地的总流中

MediaConstraints audioConstraints = createAudioConstraints();
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
//音频是数据源.创建一个音频轨道
/**
* id规则   ARDAMS + a/v + 轨道号                a:audio音频   v:video视频
*/
AudioTrack audioTrack = mPeerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
mMediaStream.addTrack(audioTrack);

3.5创建视频轨道,并添加进总流:

WebRtc自带了摄像头、屏幕共享等视频源作为码流(以下都是):

a.流程基本是一样,使用官方的VideoCapturer的子类,创建对象得到VideoCapture。
b.借助mPeerConnectionFactory.createVideoSource 创建VideoSource
c.将VideoCapture绑定到VideoSource中
d.调用videoCapturer.startCapture进行视频流的开启录制
e.调用mPeerConnectionFactory.createVideoTrack,将生成的VideoSource作为入参,得到VideoTrack视频轨道
f.将得到的视频轨道Videotrack添加进自己本地的总流即可

VideoCapturer videoCapturer = null;
mActivity.startScreenRecord(new ScreenBroadcastListener() {
    @Override
    public void getScreenIntent(Intent intent) {
        if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
            ScreenCapturerAndroid videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() {
                @Override
                public void onStop() {
                    super.onStop();
                }
            });
            //                            videoCapturer.getMediaProjection()
            //videoSource 实例化
            VideoSource videoSource = mPeerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
            //将videoCapturer绑定到videoSource中
            SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(Thread.currentThread().getName(), mRootEglBase.getEglBaseContext());
            videoCapturer.initialize(surfaceTextureHelper,mActivity,videoSource.getCapturerObserver());
            //设置预览
            videoCapturer.startCapture(1280, 720,0);//参数按顺序: 宽度、高度、关键帧间隔(多久一个I帧)
            //视频轨道关联
            VideoTrack videoTrack = mPeerConnectionFactory.createVideoTrack("ARDAMSv0",videoSource);
            mMediaStream.addTrack(videoTrack);
            //                            if (mActivity != null) {
            //                                //播放自己的流
            //                                mActivity.onSetLocalStream(mMediaStream,myId);
            //                            }
        }
    }
});
if (Camera2Enumerator.isSupported(mActivity)){
    Camera2Enumerator camera2Enumerator = new Camera2Enumerator(mActivity);
    videoCapturer=createCameraCapture(camera2Enumerator);
}else{
    Camera1Enumerator camera1Enumerator = new Camera1Enumerator(true);
    videoCapturer = createCameraCapture(camera1Enumerator);
}

/**
* 获取前置摄像头     如果没有则使用后置摄像头
* @param cameraEnumerator
* @return   null:没有找到摄像头
*/
private VideoCapturer createCameraCapture(CameraEnumerator cameraEnumerator) {
    String[] deviceNames = cameraEnumerator.getDeviceNames();
for (String deviceName : deviceNames) {
    if (cameraEnumerator.isFrontFacing(deviceName)){
        //前置摄像头
        VideoCapturer videoCapturer=cameraEnumerator.createCapturer(deviceName, this);
        if (videoCapturer!=null){
            return videoCapturer;
        }
    }
}
for (String deviceName : deviceNames) {
    if (!cameraEnumerator.isFrontFacing(deviceName)) {
        VideoCapturer videoCapturer=cameraEnumerator.createCapturer(deviceName,this);
        if (videoCapturer != null) {
            return videoCapturer;
        }
    }
}
return null;
}

3.6、创建Peer对象,用来存储当前客户端所链接的客户端的PC对象及链接处理。

Peer实例对象能存在多个,一个Peer实例对象就代表与一个设备的链接。
在创建Peer实例对象时,在其构造方法中会去创建一个PeerConnection链接。在创建时将我们前面的turn服务器地址给配置了进去。
此时这个链接只是被创建了,还需要进行本地sdp、远端sdp的配置与ICE信令的交换后才能真正的与目标设备链接成功。
Peer对象的创建是通过房间服务器,在进入房间时、以及新设备加入房间后会得到新的设备ID列表,并根据ID数量创建对应数量的peer空链接。

private class Peer implements SdpObserver, PeerConnection.Observer{
    private PeerConnection pc;
    private String targetSocketId;

    public Peer(String targetSocketId) {
        this.targetSocketId = targetSocketId;
        pc = createPeerConnection();
    }

    private PeerConnection createPeerConnection(){
        if (mPeerConnectionFactory == null) {
            mPeerConnectionFactory=createConnectionFactory();
        }
        //创建PeerConnection
        PeerConnection.RTCConfiguration rtcConfiguration = new PeerConnection.RTCConfiguration(mICEServers);//mICEServers 服务器可以有多个,如果第一个失败则会找下一个
        return mPeerConnectionFactory.createPeerConnection(rtcConfiguration,this);
    }
}

3.7、将我们本地的视频流绑定到我们创建的PeerConnection对象中。

下方代码mConnectionPeerDic参数负责存储我们创建的所有PeerConnection对象,通过遍历mConnectionPeerDic来给每一个链接添加我们的本地总流进去,这样在建立连接后,远端的客户端就能够接收到本地总流的内容。

private void addLocalStreamsPushTwoOtherDevice() {
    for (Map.Entry<String, Peer> entry : mConnectionPeerDic.entrySet()) {
        if (mMediaStream == null && mIsSpeaker) {
            createLoaclStream();
        }
        entry.getValue().pc.addStream(mMediaStream);
    }
}

3.8、因为推流涉及到多个流程。所以分为主叫端和被叫端来讲。房间服务器的通信部分则尽可能的省略代码。

3.8.1、主叫端创建主叫邀请
/**
* 发送hello
*/
private void createOffers() {
    for (Map.Entry<String, Peer> entry : mConnectionPeerDic.entrySet()) {
        //此时是主叫
        role=Role.Caller;
        Peer peer = entry.getValue();
        //创建要发送给给其他客户端的打招呼消息,用于让当前路由存储对方设备的地址到路由表中
        peer.pc.createOffer(peer,offerOrAnswerConstraint());//获取本地到其他客户端路由的sdp  获取成功以后,会走 onCreateSuccess
    }
}

private MediaConstraints offerOrAnswerConstraint() {
    MediaConstraints mediaConstraints = new MediaConstraints();
    ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
    keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
    keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", String.valueOf(videoEnable)));
    mediaConstraints.mandatory.addAll(keyValuePairs);
    return mediaConstraints;
}
3.8.2、主叫创建完成邀请信息后,会走onCreateSuccess回调,我们可以在该回调中,拿到自己的sdp信息。

调用pc.setLocalDescription设置本主叫端的sdp信息

/**
*
* @param origSdp  包含路由的地址,音频视频所走的端口
*/
@Override
public void onCreateSuccess(SessionDescription origSdp) {
    Log.e(TAG, "onCreateSuccess:    SDP回写成功");
    //需要设置好双方的sdp,即才建立联系,当前只设置了本地,还需要设置远端的setRemoteDescription
    String newSdp = preferCodec(origSdp.description,VIDEO_CODEC_H264, false);
    //设置本地的sdp
    pc.setLocalDescription(Peer.this,new SessionDescription(origSdp.type,newSdp)); //设置成功则走 onSetSuccess    设置失败则走  onSetFailure
}
3.8.3、主叫端设置setLocalDescription( )成功后,会走onSetSuccess()回调 此时主讲端再通过房间服务器,将自身的sdp发送给被叫客户端

3.8.4、被叫端收到房间服务器转发来的主叫端的sdp信息。

此时将主叫端的sdp设置进我们的setRemoteDescription()远端sdp中。

因为被叫端在初始化Peer对象时就已经设置了自己的sdp。
所以才设置完成远端的sdp后,即完成了SDP的交换。

private void handleOffer(Map map) {
    Log.e(TAG, "handleOffer: ");
    Map data = (Map) map.get("data");
    Map sdpDic;
    if (data != null) {
        sdpDic = (Map) data.get("sdp");
        String socketId = (String) data.get("socketId");
        String sdp = (String) sdpDic.get("sdp");
        mPeerConnectionManager.onReceiveOffer(socketId, sdp);
    }
}

public void onReceiveOffer(String socketId, String sdpStr) {
    mExecutor.execute(new Runnable() {
        @Override
        public void run() {
            //                 角色  1    主叫  被叫  2
            role = Role.Receiver;
            Peer mPeer = mConnectionPeerDic.get(socketId);
            SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, sdpStr);
            if (mPeer != null) {
                mPeer.pc.setRemoteDescription(mPeer, sdp);//此时会走当前客户端的setSuccess,同样主叫端的setSuccess也会被触发。
            }
        }
    });
}
3.8.5、被叫端通知WebRtc请求对方的路由器,并设置ICE
if (signalingState==PeerConnection.SignalingState.HAVE_REMOTE_OFFER){
    //因为初始化时就设置本地的sdp,所以走到这个判断代码块时,本地的sdp肯定设置好了。
    //设置了远端的sdp
    //通知webRtc请求对方的路由器:设置ICE
    pc.createAnswer(Peer.this, offerOrAnswerConstraint());
}

private MediaConstraints offerOrAnswerConstraint() {
    MediaConstraints mediaConstraints = new MediaConstraints();
    ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
    keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
    keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", String.valueOf(videoEnable)));
    mediaConstraints.mandatory.addAll(keyValuePairs);
    return mediaConstraints;
}
3.8.6、此时被叫端打洞完成,准备将被叫端的sdp发送给主叫端。

**在setSenderSetting()中进行当前通道发送端口的信息配置,用于配置编码参数,如码率控制、帧数控制等参数。(尽量主动配置码率和帧率,不然会出现投屏帧率很低问题)
**打洞完成后,通过房间服务器将被叫端的sdp发送给主叫端。

if (signalingState==PeerConnection.SignalingState.STABLE){
    //ice打洞完成,交换完了
    //开始链接
    //设置发送的配置信息
    setSenderSetting();
    if (role == Role.Receiver) {
        Log.e(TAG, "onSetSuccess: 打洞完成,把自己的sdp发送给主叫");
        mWebSocketManager.sendAnswer(targetSocketId, pc.getLocalDescription().description);
    }
}

/**
 * 设置发送的配置信息
 */
private void setSenderSetting() {
    Log.e(TAG, "Peer:  setSenderSetting    getSenders:"+ JSON.toJSON(pc.getSenders()));
    for (RtpSender sender : pc.getSenders()) {
        if (sender.track().kind().equals("video")) {
            RtpParameters parameters = sender.getParameters();
            parameters.degradationPreference=MAINTAIN_FRAMERATE;//保证帧率平稳优先
            if (parameters.encodings.size()>0) {
                RtpParameters.Encoding encoding = parameters.encodings.get(0);
                encoding.maxFramerate = 30;//最高帧数
                encoding.maxBitrateBps = 3000 * BPS_IN_KBPS;//最大码率  720p建议2000-3000kbs之间
                encoding.minBitrateBps = 2000 * BPS_IN_KBPS;//最低码率
                sender.setParameters(parameters);
            }
        }
    }
}
3.8.7、主叫端收到被叫发来的sdp信息后,配置远端sdp地址,重新走上一个步骤8.6(不过这次是作为主叫走的,所以不需要再通过房间服务器发送自己的sdp信息了)
3.8.8、双方交换完sdp后,打洞服务器开始执行,他会走onIceCandidate()回调,此时主叫和被叫端都会得到当前端生成的ICE,再通过房间服务器将ICE发送给对方。
/**
* 指挥客户端A  进行请求对方的服务器
* @param iceCandidate  本机----我到我的路由器所要经过的地址
*/
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
    Log.e(TAG, "onIceCandidate: "+iceCandidate);
	mWebSocketManager.sendIceCandidate(targetSocketId,iceCandidate);
}

在通过房间服务器转发后收到对方的ICE,给当前链接的PeerConnection,调用addIceCandidate函数,添加对方客户端的ICE。此时完成ICE交换。

/**
* 尝试在路由中添加这个记录
* @param socketId
* @param iceCandidate
*/
public void onRemoteIceCandidate(String socketId, IceCandidate iceCandidate) {
    mExecutor.execute(new Runnable() {
        @Override
        public void run() {
            Peer peer = mConnectionPeerDic.get(socketId);
            if (peer != null) {
                //                    调用addIceCandidate 当前客户端会在内部请求路由器,使路由器的路由表中存储对方客户端的路由信息
                peer.pc.addIceCandidate(iceCandidate);
            }
        }
    });
}

3.9、获取到远端的流。

在通过上述步骤,完成sdp、ice的交换后,主叫与被叫端的链接才算是真正建立,建立完成后会将本地mediaStream的本地总流的数据推送给对方。

ice交换完成后会走这个回调onAddStream()

/**
* ice交换完成后会回调这个函数
* @param mediaStream 远端的流
*/
@Override
    public void onAddStream(MediaStream mediaStream) {
    Log.e(TAG, "onAddStream: ");
mActivity.onAddRoomStream(mediaStream,this.targetSocketId);
}

3.10、渲染WebRtc接收到的流MediaStream

此时将mediaStram的流给到WebRtc自带的一个Surface组件SurfaceViewRenderer中进行视频的渲染展示

<org.webrtc.SurfaceViewRenderer
    android:id="@+id/surface_view_renderer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
//        //使用SurfaceViewRenderer创建SurfaceView
//        SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this);

        //初始化SurfaceViewRenderer
        mBinding.surfaceViewRenderer.init(mRootEglBase.getEglBaseContext(),null);
        //设置缩放模式
        mBinding.surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
        //设置视频流是否应该水平镜像
        mBinding.surfaceViewRenderer.setMirror(false);
        //是否打开硬件进行拉伸
//        mBinding.surfaceViewRenderer.setEnableHardwareScaler(false);
//        mBinding.surfaceViewRenderer.setFpsReduction(30);
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        mBinding.surfaceViewRenderer.setLayoutParams(layoutParams);

这样一个WebRtc的链接流程就基本描述完成了。可以发现,集成WebRtc的SDK后,是可以不去接触码流,不去接触MediaCodec,编解码、音频增益降噪等都交给了WebRtc来进行处理。

WebRtc很好的解决音视频通讯的即时性问题。

Demo(Android)gitee.com/linxunyou/W…

(Demo中使用的服务器地址是在公网搭建的云服务器,服务器有效期一个月,现已过期。 服务器部署资料:WebRtc所需服务器搭建流程
相关资料:见第一部分参考文献