WebRTC视频Android客户端

1、关于WebRTC这个库,虽然说它提供了点对点的通信,但是前提也是要双方都连接到服务器为基础,首先浏览器之间交换建立通信的元数据(其实也就是信令)必须要经过服务器,其次官方所说的NAT和防火墙也是需要经过服务器(其实可以理解成打洞,就是寻找建立连接的方式) 
至于服务器那边,我不懂也不多说。

关于Android客户端,你只需要了解RTCPeerConnection这个接口,该接口代表一个由本地计算机到远程端的WebRTC连接,提供了创建,保持,监控,关闭连接的方法的实现。 
我们还需要搞懂两件事情:1、确定本机上的媒体流的特性,如分辨率、编码能力等(这个其实包含在SDP描述中,后面会讲解)2、连接两端的主机的网络地址(其实就是ICE Candidate)

通过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实例对象。

所以两端视频连接的过程大致就是上述流程,通过一系列的信令交换,A和B所创建的pc实例对象都包含A和B的SDP描述符,完成了以上两件事情中的第一件事情,那么第二件事情就是获取连接两端主机的网络地址啦,如下:

通过ICE框架建立NAT/防火墙穿越的连接(打洞) 
这个网址应该是能从外界直接访问的,WebRTC使用了ICE框架来获得这个网址, 
PeerConnection在创立的时候可以将ICE服务器的地址传递进去,如: 
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()可以在连接完成之前,在连接建立之后,对方一样能监听到媒体流。

下面是我运用sdk所做的代码实现流程: 
1、首先在界面布局中,xml文件中所要显示视频的地方写好GLSurfaceView控件,当然你也可以动态添加该控件(我写成了静态的了,这个随意) 
2、首先先初始化该控件,即:(当然刚进入界面就初始化也可以,后面连接服务器之后再初始化也可以,顺序都行) 
public void initPlayView(GLSurfaceView glSurfaceView) { 
VideoRendererGui.setView(glSurfaceView, (Runnable)null); 
this.isVideoRendererGuiSet = true; 

这一步就是要把glSurfaceView添加VideoRendererGui中,作为要显示的界面 
3、登录到视频服务器,这一步其实应该是最开始的(地2、3步骤顺序不限) 
public void connect(String url) throws URISyntaxException { 
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; 


先初始化配置网络ping的一些信息,然后在连接服务器: 
client.connect();

登录的时候,设置一下token的一些监听: 
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回调收到你发送的本地流)

4、当A主动请求B开始视频聊天时,则需要手动调用: 
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下),或者同一网络运营商之间的互通,至于其他的不同网络运营商之间的互通并不保证百分百互通,所以这个是个难题。

你可能感兴趣的:(即时通讯)