關於WebRTC視訊Android端實現的原理解讀

艾神一不小心發表於2018-03-30

簡介:

進入公司之後做了第一個專案就是關於視訊的,因為用的是別人提供的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/…

注:整個專案中包含了上面的視訊實現程式碼(專案中的功能比較多)

相關文章