WebRTC提供了点对点之间的通信,但并不意味着WebRTC不需要服务器。暂且不说基于服务器的一些扩展业务,WebRTC至少有两件事必须要用到服务器:
1. 浏览器之间交换建立通信的元数据(信令)必须通过服务器
2. 为了穿越NAT和防火墙
此处,我们使用XMPP协议实现信令,采用openfire当做服务器,通过openfire服务器+Smack API实现信令的传递。
因此,在建立PeerConnection实例之后,想要使用其建立一个点对点的信道,我们需要做两件事:
1. 确定本机上的媒体流的特性,比如分辨率、编解码能力啥的(SDP描述符)
2. 连接两端的主机的网络地址(ICE Candidate)
通过offer和answer交换SDP描述符
大致上在两个用户(甲和乙)之间建立点对点连接流程应该是这个样子(这里不考虑错误的情况,PeerConnection简称PC):
甲和乙各自建立一个PC实例
甲通过PC所提供的createOffer()方法建立一个包含甲的SDP描述符的offer信令
甲通过PC所提供的setLocalDescription()方法,将甲的SDP描述符交给甲的PC实例
甲将offer信令通过服务器发送给乙
乙将甲的offer信令中所包含的的SDP描述符提取出来,通过PC所提供的setRemoteDescription()方法交给乙的PC实例
乙通过PC所提供的createAnswer()方法建立一个包含乙的SDP描述符answer信令
乙通过PC所提供的setLocalDescription()方法,将乙的SDP描述符交给乙的PC实例
乙将answer信令通过服务器发送给甲
甲接收到乙的answer信令后,将其中乙的SDP描述符提取出来,调用setRemoteDescripttion()方法交给甲自己的PC实例
通过在这一系列的信令交换之后,甲和乙所创建的PC实例都包含甲和乙的SDP描述符了,完成了两件事的第一件。我们还需要完成第二件事——获取连接两端主机的网络地址
通过ICE框架建立NAT/防火墙穿越的连接
这个网络地址应该是能从外界直接访问,WebRTC使用ICE框架来获得这个地址。PeerConnection在创立的时候可以将ICE服务器的地址传递进去,如:
 private List<PeerConnection.IceServer> getIceServers(String url,String user,String credential)
    {
        PeerConnection.IceServer turn = new PeerConnection.IceServer(
                url,user,credential);
        LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
        iceServers.add(turn);
        return iceServers;
    }
//iceServer List对象获取
        List<PeerConnection.IceServer> iceServers = getIceServers(CoturnData.url,
                CoturnData.userName,CoturnData.credential);
        pcConstraints = new MediaConstraints();
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "DtlsSrtpKeyAgreement", "true"));
        pcConstraints.mandatory.add(new
                MediaConstraints.KeyValuePair("VoiceActivityDetection", "false"));
        pc = factory.createPeerConnection(iceServers,pcConstraints,pcObserver);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当然这个地址也需要交换,还是以甲乙两位为例,交换的流程如下(PeerConnection简称PC):
甲、乙各创建配置了ICE服务器的PC实例,并为其添加onicecandidate事件回调
当网络候选可用时,将会调用onicecandidate函数
在回调函数内部,甲或乙将网络候选的消息封装在ICE Candidate信令中,通过服务器中转,传递给对方
甲或乙接收到对方通过服务器中转所发送过来ICE Candidate信令时,将其解析并获得网络候选,将其通过PC实例的addIceCandidate()方法加入到PC实例中
这样连接就创立完成了,可以向RTCPeerConnection中通过addStream()加入流来传输媒体流数据。将流加入到RTCPeerConnection实例中后,对方就可以通过onaddstream所绑定的回调函数监听到了。调用addStream()可以在连接完成之前,在连接建立之后,对方一样能监听到媒体流
代码实现:
public class VideoCallActivity extends ParentActivity{
    public static final String VIDEO_TRACK_ID = "video_track_id";
    public static final String AUDIO_TRACK_ID = "audio_track_id";
    public static final String LOCAL_MEDIA_STREAM_ID = "local_media_stream_id";
    private String mServiceName;    //XMPP服务器名称
    private GLSurfaceView mGLSurfaceView;
    private GLSurfaceView mGLSurfaceViewRemote;
    private PeerConnection pc;
    private final PCObserver pcObserver = new PCObserver();
    private final SDPObserver sdpObserver = new SDPObserver();
    private MediaConstraints sdpMediaConstraints;
    private MediaConstraints pcConstraints;
    private String remoteName;
    IceCandidate remoteIceCandidate;
    private boolean mIsInited;
    private boolean mIsCalled;
    PeerConnectionFactory factory;
    VideoCapturer videoCapturer;
    VideoSource videoSource;
    VideoRenderer localVideoRenderer;
    VideoRenderer remoteVideoRenderer;
    AudioManager audioManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_call);
        //打开扬声器
        audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
        audioManager.setSpeakerphoneOn(true);
        mServiceName = connection.getServiceName();
        mGLSurfaceView = (GLSurfaceView) findViewById(R.id.glsurfaceview);
//        mGLSurfaceViewRemote = (GLSurfaceView) findViewById(R.id.glsurfaceview_remote);
        //检查初始化音视频设备是否成功
        if (!PeerConnectionFactory.initializeAndroidGlobals(this,true,true,true,null))
        {
            Log.e("init","PeerConnectionFactory init fail!");
            return;
        }
//        Intent intent = getIntent().getBundleExtra()
        //Media条件信息SDP接口
        sdpMediaConstraints = new MediaConstraints();
        //接受远程音频
        sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "OfferToReceiveAudio", "true"));
        //接受远程视频
        sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "OfferToReceiveVideo", "true"));
        factory = new PeerConnectionFactory();
        //iceServer List对象获取
        List<PeerConnection.IceServer> iceServers = getIceServers(CoturnData.url,
                CoturnData.userName,CoturnData.credential);
        pcConstraints = new MediaConstraints();
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
                "DtlsSrtpKeyAgreement", "true"));
        pcConstraints.mandatory.add(new
                MediaConstraints.KeyValuePair("VoiceActivityDetection", "false"));
        pc = factory.createPeerConnection(iceServers,pcConstraints,pcObserver);
        mIsInited = false;
        mIsCalled=false;
        boolean offer=getIntent().getBooleanExtra("createOffer",false);
        //offer:如果offer为true表示主叫方初始化,如果为false表示被叫方初始化。
        remoteName = getIntent().getStringExtra("remoteName");
        if(!offer)
        {
            initialSystem();
        }
        else {
            callRemote(remoteName);
        }
        //当VideoActivity已经打开时,处理后续的intent传过来的数据。
        processExtraData();
    }
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        processExtraData();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //通话结束,发送通话结束消息
        videoCallEnded();
        //释放资源
        videoCapturer.dispose();
        videoSource.stop();
        if (pc != null) {
            pc.dispose();
            pc = null;
        }
        audioManager.setSpeakerphoneOn(false);
    }
    private void videoCallEnded() {
        String chatJid = remoteName+"@"+mServiceName;
        Message message = new Message();
        VideoInvitation videoInvitation = new VideoInvitation();
        videoInvitation.setTypeText("video-ended");
        message.addExtension(videoInvitation);
        Chat chat = createChat(chatJid);
        try {
            chat.sendMessage(message);
        } catch (SmackException.NotConnectedException e) {
            e.printStackTrace();
        }
    }
    private void processExtraData() {
        Intent intent = getIntent();
        //获取SDP数据
        String sdpType = intent.getStringExtra("type");
        String sdpDescription = intent.getStringExtra("description");
        if (sdpType != null)
        {
            SessionDescription.Type type = SessionDescription.Type.fromCanonicalForm(sdpType);
            SessionDescription sdp = new SessionDescription(type,sdpDescription);
            if (pc == null)
            {
                Log.e("pc","pc == null");
            }
            pc.setRemoteDescription(sdpObserver,sdp);
            //如果是offer,则被叫方createAnswer
            if (sdpType.equals("offer"))
            {
                mIsCalled = true;
                pc.createAnswer(sdpObserver,sdpMediaConstraints);
            }
        }
        //获取ICE Candidate数据
        String iceSdpMid = intent.getStringExtra("sdpMid");
        int iceSdpMLineIndex = intent.getIntExtra("sdpMLineIndex",-1);
        String iceSdp = intent.getStringExtra("sdp");
        if (iceSdpMid != null)
        {
            IceCandidate iceCandidate = new IceCandidate(iceSdpMid,iceSdpMLineIndex,iceSdp);
            if (remoteIceCandidate == null)
            {
                remoteIceCandidate = iceCandidate;
            }
            //下面这步放到函数drainRemoteCandidates()中
            /*//添加远端的IceCandidate到pc
            pc.addIceCandidate(iceCandidate);*/
        }
        //结束activity
        boolean videoEnded = intent.getBooleanExtra("videoEnded",false);
        if (videoEnded)
        {
            finish();
        }
    }
    private void callRemote(String remoteName) {
        initialSystem();
        //createOffer
        pc.createOffer(sdpObserver,sdpMediaConstraints);
    }
    private void initialSystem() {
        if (mIsInited)
        {
            return;
        }
        //获取前置摄像头本地视频流
        String frontDeviceName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
//        String frontDeviceName = "Camera 1, Facing front, Orientation 0";
        Log.e("CameraName","CameraName: "+frontDeviceName);
        videoCapturer = VideoCapturerAndroid.create(frontDeviceName);
        if (videoCapturer == null)
        {
            Log.e("open","fail to open camera");
            return;
        }
        //视频
        MediaConstraints mediaConstraints = new MediaConstraints();
        videoSource = factory.createVideoSource(videoCapturer, mediaConstraints);
        VideoTrack localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
        //音频
        MediaConstraints audioConstraints = new MediaConstraints();
        AudioSource audioSource = factory.createAudioSource(audioConstraints);
        AudioTrack localAudioTrack = factory.createAudioTrack(AUDIO_TRACK_ID,audioSource);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            }
        };
        VideoRendererGui.setView(mGLSurfaceView,runnable);
        try {
            //改成ScalingType.SCALE_ASPECT_FILL可以显示双方视频,但是显示比例不美观,并且不知道最后一个参数true和false的含义。
            localVideoRenderer = VideoRendererGui.createGui(0,0,30,30, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL,true);
            remoteVideoRenderer = VideoRendererGui.createGui(0,0,100,100, VideoRendererGui.ScalingType.SCALE_FILL,true);
            localVideoTrack.addRenderer(localVideoRenderer);
        } catch (Exception e) {
            e.printStackTrace();
        }
        MediaStream localMediaStream = factory.createLocalMediaStream(LOCAL_MEDIA_STREAM_ID);
        localMediaStream.addTrack(localAudioTrack);
        localMediaStream.addTrack(localVideoTrack);
        pc.addStream(localMediaStream);
    }
    private List<PeerConnection.IceServer> getIceServers(String url,String user,String credential)
    {
        PeerConnection.IceServer turn = new PeerConnection.IceServer(
                url,user,credential);
        LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
        iceServers.add(turn);
        return iceServers;
    }
    private class PCObserver implements PeerConnection.Observer
    {
        @Override
        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
        }
        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
        }
        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
        }
        //发送ICE候选到其他客户端
        @Override
        public void onIceCandidate(final IceCandidate iceCandidate) {
            //利用XMPP发送iceCandidate到其他客户端
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    String chatJid = remoteName+"@"+mServiceName;
                    Message message = new Message();
                    IceCandidateExtensionElement iceCandidateExtensionElement=
                            new IceCandidateExtensionElement();
                    iceCandidateExtensionElement.setSdpMidText(iceCandidate.sdpMid);
                    iceCandidateExtensionElement.setSdpMLineIndexText(iceCandidate.sdpMLineIndex);
                    iceCandidateExtensionElement.setSdpText(iceCandidate.sdp);
                    message.addExtension(iceCandidateExtensionElement);
                    Chat chat = createChat(chatJid);
                    try {
                        chat.sendMessage(message);
                    } catch (SmackException.NotConnectedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        //Display a media stream from remote
        @Override
        public void onAddStream(final MediaStream mediaStream) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (pc == null)
                    {
                        Log.e("onAddStream","pc == null");
                        return;
                    }
                    if (mediaStream.videoTracks.size()>1 || mediaStream.audioTracks.size()>1)
                    {
                        Log.e("onAddStream","size > 1");
                        return;
                    }
                    if (mediaStream.videoTracks.size() == 1)
                    {
                        VideoTrack videoTrack = mediaStream.videoTracks.get(0);
                        videoTrack.addRenderer(remoteVideoRenderer);
                    }
                }
            });
        }
        @Override
        public void onRemoveStream(final MediaStream mediaStream) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mediaStream.videoTracks.get(0).dispose();
                }
            });
        }
        @Override
        public void onDataChannel(DataChannel dataChannel) {
        }
        @Override
        public void onRenegotiationNeeded() {
        }
    }
    private class SDPObserver implements SdpObserver
    {
        @Override
        public void onCreateSuccess(final SessionDescription sessionDescription) {
            //sendMessage(offer);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    String chatJid = remoteName+"@"+mServiceName;
                    Message message = new Message();
                    SDPExtensionElement sdpExtensionElement = new SDPExtensionElement();
                    sdpExtensionElement.setTypeText(sessionDescription.type.canonicalForm());
                    sdpExtensionElement.setDescriptionText(sessionDescription.description);
                    message.addExtension(sdpExtensionElement);
                    Chat chat = createChat(chatJid);
                    try {
                        chat.sendMessage(message);
                    } catch (SmackException.NotConnectedException e) {
                        e.printStackTrace();
                    }
                    pc.setLocalDescription(sdpObserver,sessionDescription);
                }
            });
        }
        @Override
        public void onSetSuccess() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //主叫方
                    if (!mIsCalled)
                    {
                        if (pc.getRemoteDescription() != null)
                        {
                            drainRemoteCandidates();
                        }
                    }
                    //被叫方
                    else
                    {
                        //如果被叫方还没有createAnswer
                        if (pc.getLocalDescription() == null)
                        {
                            Log.e("SDPObserver", "SDPObserver create answer");
                        }
                        else
                        {
                            drainRemoteCandidates();
                        }
                    }
                }
            });
        }
        private void drainRemoteCandidates() {
            if (remoteIceCandidate == null)
            {
                Log.e("SDPObserver","remoteIceCandidate == null");
                return;
            }
            pc.addIceCandidate(remoteIceCandidate);
            Log.e("IceCanditate","添加IceCandidate成功");
            remoteIceCandidate = null;
        }
        @Override
        public void onCreateFailure(String s) {
            Log.e("SDPObserver","onCreateFailure");
        }
        @Override
        public void onSetFailure(String s) {
            Log.e("SDPObserver","onSetFailure");
        }
    }
}
————————————————
版权声明:本文为CSDN博主「程序员的自我救赎」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011026329/article/details/50582690

Android之WebRTC介绍(二)的更多相关文章

  1. Android之WebRTC介绍

    参考自:Introduction to WebRTC on AndroidAndroid之WebRTC介绍 WebRTC被誉为是web长期开源开发的一个新启元,是近年来web开发的最重要创新.WebR ...

  2. Android端WebRTC点对点互连

    项目准备 信令服务器代码:https://github.com/matthewYang92/WebRtcServer(代码改自ProjectRTC) 安装Node.js 进入项目根目录,命令行:npm ...

  3. Android IOS WebRTC 音视频开发总结(四六)-- 从另一个角度看国内首届WebRTC大会

    文章主要从开发者角度谈国内首届WebRTC大会,支持原创,文章来自博客园RTC.Blacker,支持原创,转载必须说明出处,更多详见www.rtc.help. -------------------- ...

  4. 转:Android IOS WebRTC 音视频开发总结 (系列文章集合)

    随笔分类 - webrtc   Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译和整理的,译 ...

  5. Android APP压力测试(二)之Monkey信息自动收集脚本

      Android APP压力测试(二) 之Monkey信息自动收集脚本 前言: 上一篇Monkey介绍基本搬抄官方介绍,主要是为了自己查阅方便.本文重点介绍我在进行Monkey时如何自动收集相关信息 ...

  6. Android项目实战(二十八):Zxing二维码实现及优化

    前言: 多年之前接触过zxing实现二维码,没想到今日项目中再此使用竟然使用的还是zxing,百度之,竟是如此牛的玩意. 当然,项目中我们也许只会用到二维码的扫描和生成两个功能,所以不必下载完整的ja ...

  7. Android IOS WebRTC 音视频开发总结(八十五)-- 使用WebRTC广播网络摄像头视频(下)

    本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...

  8. Android IOS WebRTC 音视频开发总结(八十三)-- 使用WebRTC广播网络摄像头视频(上)

    本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...

  9. Android多线程分析之二:Thread的实现

    Android多线程分析之二:Thread的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处   在前文<Android多线程分析之一 ...

随机推荐

  1. Windows Server 2012 R2 远程桌面自动设置为不允许连接问题解决方案

    用“gpedit.msc”调出策略组设置窗口,在策略组界面点击:计算机配置->管理模块->Windows组件->远程桌面服务->远程桌面会话主机->连接->允许用户 ...

  2. Junit测试。

    Junit使用: 白盒测试 步骤: 1.定义测试类. 2.定义测试方法:可以单独运行. 3.给方法加@Test,导入junit依赖环境. 判定结果: 红色:失败  绿色:成功. 一般不看输出,而是使用 ...

  3. linux命令当前文件夹下面模糊搜索文件

    在当前文件夹下面模糊搜索文件: find . -type f | xargs grep 'boot',"boot"表示文件名中包含的字符串

  4. zabbix--基础概念及原理

    zabbix 基础概念及工作原理整理 什么是 zabbix? Zabbix 能监控各种网络参数,保证服务器系统的安全运营:并提供灵活的通知机制以让系统管理员快速定位.解决存在的各种问题.是一个基于 W ...

  5. go mod开发模式设置

    文章要解决的仅仅是一个问题 当你使用go get 无论如何get不到所需的包的问题 第一步就是下载goland 新手极其推荐goland,因为直接使用gland几乎没有挫败感,使用其他工具可能要折腾好 ...

  6. BSGS学习笔记

    用于求\(A^{x} \equiv B \pmod{C}\) 高次方程的最小正整数解x,其中C为素数 引理1:$a^{i\mod\varphi(p) } \equiv a^{i} $ (mod p) ...

  7. Python线程join和setDaemon

    看一下线程的setDaemon()方法 import time import threading import ctypes import inspect def sayHello(): for i ...

  8. cortex-m系列的区别(图解)及今日碎片学习笔记

    下图转自https://www.cnblogs.com/luckytimor/p/6747026.html 该系列的结构都是哈佛结构,而且目前可以使用keil来开发,而且keil mdk不能开发R.A ...

  9. vue-element-admin

    https://github.com/deadzq/vue-element-admin-1.git vue-element-admin使用. cnpm install npm run dev

  10. Apache ServiceComb Pack 微服务分布式数据最终一致性解决方案

    https://github.com/OpenSagas-csharp/servicecomb-pack-csharp Saga基本使用指南 使用前置条件说明 如果还有同学对Saga还不甚了解的同学, ...