进入公司以后作了第一个项目就是关于视频的,由于用的是别人提供的sdk,因此说很容易就能实现其中的功能,那么项目结尾的时候就想着不能光会用,起码得知道原理过程吧!那么下面就讲解一下本人对关于WebRTC的视频链接过程的一些讲解:android
关于WebRTC这个库,虽说它提供了点对点的通讯,可是前提也是要双方都链接到服务器为基础,首先浏览器之间交换创建通讯的元数据(其实也就是信令)必需要通过服务器,其次官方所说的NAT和防火墙也是须要通过服务器(其实能够理解成打洞,就是寻找创建链接的方式) 至于服务器那边,我不太懂也很少说。git
这里提供一个已经编译好的WebRTC项目,不然刚入门的小伙伴估计很难去本身编译。关于android客户端,你只须要了解RTCPeerConnection这个接口,该接口表明一个由本地计算机到远程端的WebRTC链接,提供了建立,保持,监控,关闭链接的方法的实现。 咱们还须要搞懂两件事情:一、肯定本机上的媒体流的特性,如分辨率、编码能力等(这个其实包含在SDP描述中,后面会讲解)二、链接两端的主机的网络地址(其实就是ICE Candidate)github
经过offer和answe交换SDP描述符:(好比A向B发起视频请求) 好比A和B须要创建点对点的链接,大概流程就是:两端先各自创建一个PeerConnection实例(这里称为pc),A经过pc所提供的createOffer()方法创建一个包含SDP描述符的offer信令,一样A经过pc提供的setLocalDescription()方法,将A的SDP描述符交给A的pc对象,A将offer信令经过服务器发送给B。B将A的offer信令中所包含的SDP描述符提取出来,经过pc所提供的setRemoteDescription()方法交给B的pc实例对象,B将pc所提供的createAnswer()方法创建一个包含B的SDP描述符answer信令,B经过pc提供的setLocalDescription()方法,将本身的SDP描述符交给本身的pc实例对象,而后将answer信令经过服务器发送给A,最后A接收到B的answer信令后,将其中的SDP描述符提取出来,调用setRemoteDescription()方法交给A本身的pc实例对象。web
因此两端视频链接的过程大体就是上述流程,经过一系列的信令交换,A和B所建立的pc实例对象都包含A和B的SDP描述符,完成了以上两件事情中的第一件事情,那么第二件事情就是获取链接两端主机的网络地址啦,以下:浏览器
经过ICE框架创建NAT/防火墙穿越的链接(打洞) 这个网址应该是能从外界直接访问的,WebRTC使用了ICE框架来得到这个网址, PeerConnection在创立的时候能够将ICE服务器的地址传递进去,如:bash
private void init(Context context) { PeerConnectionFactory.initializeAndroidGlobals(context, true, true, true); this.factory = new PeerConnectionFactory(); this.iceServers.add(new IceServer("turn:turn.realtimecat.com:3478", "learningtech", "learningtech")); } 注意:“turn:turn.realtimecat.com:3478”这段字符其实就是该ICE服务器的地址。 复制代码
固然这个地址也须要交换,仍是以AB两位为例,交换的流程以下(PeerConnection简称PC): A、B各建立配置了ICE服务器的PC实例,并为其添加onicecandidate事件回调 当网络候选可用时,将会调用onicecandidate函数 在回调函数内部,A或B将网络候选的消息封装在ICE Candidate信令中,经过服务器中转,传递给对方 A或B接收到对方经过服务器中转所发送过来ICE Candidate信令时,将其解析并得到网络候选,将其经过PC实例的addIceCandidate()方法加入到PC实例中。服务器
这样链接就创建完成了,能够向RTCPeerConnection中经过addStream()加入流来传输媒体流数据。将流加入到RTCPeerConnection实例中后,对方就能够经过onaddstream所绑定的回调函数监听到了。调用addStream()能够在链接完成以前,在链接创建以后,对方同样能监听到媒体流。markdown
下面是我运用sdk所作的代码实现流程:网络
public void initPlayView(GLSurfaceView glSurfaceView) { VideoRendererGui.setView(glSurfaceView, (Runnable)null); this.isVideoRendererGuiSet = true; } 复制代码
这一步就是要把glSurfaceView添加VideoRendererGui中,做为要显示的界面。session
public void connect(String url) throws URISyntaxException { //先初始化配置网络ping的一些信息 this.init(url); //而后在链接服务器 this.client.connect(); } private void init(String url) throws URISyntaxException { if (!this.init) { Options opts = new Options(); opts.forceNew = true; opts.reconnection = false; opts.query = "user_id=" + this.username; this.client = IO.socket(url, opts); this.client.on("connect", new Listener() { public void call(Object... args) { if (Token.this.mEventHandler != null) { Message msg = Token.this.mEventHandler.obtainMessage(10010); Token.this.mEventHandler.sendMessage(msg); } } }).on("disconnect", new Listener() { public void call(Object... args) { if (Token.this.mEventHandler != null) { Message msg = Token.this.mEventHandler.obtainMessage(10014); Token.this.mEventHandler.sendMessage(msg); } } }).on("error", new Listener() { public void call(Object... args) { if (Token.this.mEventHandler != null) { Error error = null; if (args.length > 0) { try { error = (Error) (new Gson()).fromJson((String) args[0], Error.class); } catch (Exception var4) { var4.printStackTrace(); } } Message msg = Token.this.mEventHandler.obtainMessage(10013, error); Token.this.mEventHandler.sendMessage(msg); } } }).on("connect_timeout", new Listener() { public void call(Object... args) { if (Token.this.mEventHandler != null) { Message msg = Token.this.mEventHandler.obtainMessage(10012); Token.this.mEventHandler.sendMessage(msg); } } }).on("connect_error", new Listener() { public void call(Object... args) { if (Token.this.mEventHandler != null) { Message msg = Token.this.mEventHandler.obtainMessage(10011); Token.this.mEventHandler.sendMessage(msg); } } }).on("message", new Listener() { public void call(Object... args) { try { Token.this.handleMessage(cn.niusee.chat.sdk.Message.parseMessage((JSONObject) args[0])); } catch (MessageErrorException var3) { var3.printStackTrace(); } } }); this.init = true; } } 复制代码
public interface OnTokenCallback {
void onConnected();//视频链接成功的回调
void onConnectFail();
void onConnectTimeOut();
void onError(Error var1);//视频链接错误的回调
void onDisconnect();//视频断开的回调
void onSessionCreate(Session var1);//视频打洞成功的回调
}
复制代码
public void login(String username) { try { SingleChatClient.getInstance(getApplication()).setOnConnectListener(new SingleChatClient.OnConnectListener() { @Override public void onConnect() { // loadDevices(); Log.e(TAG, "链接视频服务器成功"); state.setText("登陆视频服务器成功!"); } @Override public void onConnectFail(String reason) { Log.e(TAG, "链接视频服务器失败"); state.setText("登陆视频服务器失败!" + reason); } @Override public void onSessionCreate(Session session) { Log.e(TAG, "来电者名称:" + session.callName); mSession = session; accept.setVisibility(View.VISIBLE); requestPermission(new String[]{Manifest.permission.CAMERA}, "请求设备权限", new GrantedResult() { @Override public void onResult(boolean granted) { if(granted){ createLocalStream(); }else { Toast.makeText(MainActivity.this,"权限拒绝",Toast.LENGTH_SHORT).show(); } } }); mSession.setOnSessionCallback(new OnSessionCallback() { @Override public void onAccept() { Toast.makeText(MainActivity.this, "视频接收", Toast.LENGTH_SHORT).show(); } @Override public void onReject() { Toast.makeText(MainActivity.this, "拒绝通话", Toast.LENGTH_SHORT).show(); } @Override public void onConnect() { Toast.makeText(MainActivity.this, "视频创建成功", Toast.LENGTH_SHORT).show(); } @Override public void onClose() { Log.e(TAG, "onClose 我是被叫方"); hangup(); } @Override public void onRemote(Stream stream) { Log.e(TAG, "onRemote 我是被叫方"); mRemoteStream = stream; mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false)); mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false)); } @Override public void onPresence(Message message) { } }); } }); // SingleChatClient.getInstance(getApplication()).connect(UUID.randomUUID().toString(), WEB_RTC_URL); Log.e("MainActicvity===",username); SingleChatClient.getInstance(getApplication()).connect(username, WEB_RTC_URL); } catch (URISyntaxException e) { e.printStackTrace(); Log.d(TAG, "链接失败"); } } 复制代码
注意:
onSessionCreate(Session session)这个回调是当检测到有视频请求来的时候才会触发,因此这里能够设置当触发该回调是显示一个接受按钮,一个拒绝按钮,session中携带了包括对方的userName,以及各类信息(上面所说的SDP描述信息等),这个时候经过session来设置OnSessionCallback的回调信息,public interface OnSessionCallback {
void onAccept();//用户赞成
void onReject();//用户拒绝
void onConnect();//链接成功
void onClose();//链接掉开
void onRemote(Stream var1);//当远程流开启的时候,就是对方把他的本地流传过来的时候
void onPresence(Message var1);//消息通道过来的action消息,action是int型,远程控制的时候可使用这个int型信令发送指令
}
复制代码
注意:
@Override public void onRemote(Stream stream) { Log.e(TAG, "onRemote 我是被叫方"); mRemoteStream = stream; mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false)); mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false)); } 复制代码
这里当执行远程流回调过来的时候,就能够显示对方的画面,而且刷新显示本身的本地流小窗口。(最重要的前提是,若是想让对方收到本身发送的本地流,必需要本身先调用playStream,这样对方才能经过onRemote回调收到你发送的本地流)
private void call() { try { Log.e("MainActivity===","对方username:"+userName); mSession = mSingleChatClient.getToken().createSession(userName); //userName是指对方的用户名,而且这里要新建session对象,由于你是主动发起呼叫的,若是是被呼叫的则在onSessionCreate(Session session)回调中会拿到session对象的。(主叫方和被叫方不太同样) } catch (SessionExistException e) { e.printStackTrace(); } requestPermission(new String[]{Manifest.permission.CAMERA}, "请求设备相机权限", new GrantedResult() { @Override public void onResult(boolean granted) { if(granted){//表示用户容许 createLocalStream();//权限容许以后,首先打开本地流,以及摄像头开启 }else {//用户拒绝 Toast.makeText(MainActivity.this,"权限拒绝",Toast.LENGTH_SHORT).show(); return; } } }); mSession.setOnSessionCallback(new OnSessionCallback() { @Override public void onAccept() { Toast.makeText(MainActivity.this, "通话创建成功", Toast.LENGTH_SHORT).show(); } @Override public void onReject() { Toast.makeText(MainActivity.this, "对方拒绝了您的视频通话请求", Toast.LENGTH_SHORT).show(); } @Override public void onConnect() { } @Override public void onClose() { mSingleChatClient.getToken().closeSession(userName); Log.e(TAG, "onClose 我是呼叫方"); hangup(); Toast.makeText(MainActivity.this, "对方已中断视频通话", Toast.LENGTH_SHORT).show(); } @Override public void onRemote(Stream stream) { mStream = stream; Log.e(TAG, "onRemote 我是呼叫方"); Toast.makeText(MainActivity.this, "视频创建成功", Toast.LENGTH_SHORT).show(); mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false)); mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false)); } @Override public void onPresence(Message message) { } }); if (mSession != null) { mSession.call();//主动开启呼叫对方 } } 复制代码
建立本地流:
private void createLocalStream() { if (mLocalStream == null) { try { String camerName = CameraDeviceUtil.getFrontDeviceName(); if(camerName==null){ camerName = CameraDeviceUtil.getBackDeviceName(); } mLocalStream = mSingleChatClient.getChatClient().createStream(camerName, new Stream.VideoParameters(640, 480, 12, 25), new Stream.AudioParameters(true, false, true, true), null); } catch (StreamEmptyException | CameraNotFoundException e) { e.printStackTrace(); } } else { mLocalStream.restart(); } mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false)); } 复制代码
以上只是简单的讲述原理以及sdk的用法(若是你想了解该sdk,能够在下面的评论中留言,我会发给你的),之后会重点讲解更细节的原理,可是有一点更为重要的难题,就是关于多网互通的问题,及A方为联通4G状态,B方为电信WIFI状态,或者B方为移动4G状态,这种不一样网络运营商之间,互通可能存在问题,以前进行测试的时候,进行专门的抓包调试过,结果显示当A为联通4G的时候,向B(移动4G)发起视频的时候,A是一直处在打洞状态,可是一直打洞不通,并无走转发(即互联网),理论上来讲,走转发是最后一种状况,即前面的全部方式都不通,那么转发是确定通的,可是转发要涉及到架设中转服务器,这个中转服务器须要大量的带宽才可以能够保证视频链接,因此目前的视频默认支持内网(同一wifi下),或者同一网络运营商之间的互通,至于其余的不一样网络运营商之间的互通并不保证百分百互通,因此这个是个难题。
注:整个项目中包含了上面的视频实现代码(项目中的功能比较多)