簡介:
進入公司之後做了第一個專案就是關於視訊的,因為用的是別人提供的sdk,所以說很容易就能實現其中的功能,那麼專案結尾的時候就想著不能光會用,起碼得知道原理過程吧!那麼下面就講解一下本人對關於WebRTC的視訊連線過程的一些講解:
關於WebRTC這個庫,雖然說它提供了點對點的通訊,但是前提也是要雙方都連線到伺服器為基礎,首先瀏覽器之間交換建立通訊的後設資料(其實也就是信令)必須要經過伺服器,其次官方所說的NAT和防火牆也是需要經過伺服器(其實可以理解成打洞,就是尋找建立連線的方式) 至於伺服器那邊,我不太懂也不多說。
Android端:
這裡提供一個已經編譯好的WebRTC專案,否則剛入門的小夥伴估計很難去自己編譯。關於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 {
//先初始化配置網路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;
}
}
複製程式碼
4、登入的時候,設定一下token的一些監聽:
public interface OnTokenCallback {
void onConnected();//視訊連線成功的回撥
void onConnectFail();
void onConnectTimeOut();
void onError(Error var1);//視訊連線錯誤的回撥
void onDisconnect();//視訊斷開的回撥
void onSessionCreate(Session var1);//視訊打洞成功的回撥
}
複製程式碼
5、下面是我的登入連線伺服器的程式碼:
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回撥收到你傳送的本地流)
6、當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下),或者同一網路運營商之間的互通,至於其他的不同網路運營商之間的互通並不保證百分百互通,所以這個是個難題。
重中之重:github.com/Alex-Jerry/…
注:整個專案中包含了上面的視訊實現程式碼(專案中的功能比較多)