1. 程式人生 > >WebRTC視訊Android客戶端

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下),或者同一網路運營商之間的互通,至於其他的不同網路運營商之間的互通並不保證百分百互通,所以這個是個難題。