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

簡介:

進入公司以後作了第一個項目就是關於視頻的,由於用的是別人提供的sdk,因此說很容易就能實現其中的功能,那麼項目結尾的時候就想着不能光會用,起碼得知道原理過程吧!那麼下面就講解一下本人對關於WebRTC的視頻鏈接過程的一些講解:android

關於WebRTC這個庫,雖說它提供了點對點的通訊,可是前提也是要雙方都鏈接到服務器爲基礎,首先瀏覽器之間交換創建通訊的元數據(其實也就是信令)必需要通過服務器,其次官方所說的NAT和防火牆也是須要通過服務器(其實能夠理解成打洞,就是尋找創建鏈接的方式) 至於服務器那邊,我不太懂也很少說。git

Android端:

這裏提供一個已經編譯好的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所作的代碼實現流程:網絡

一、首先在界面佈局中,xml文件中所要顯示視頻的地方寫好GLSurfaceView控件,固然你也能夠動態添加該控件(我寫成了靜態的了,這個隨意)

二、首先先初始化該控件,即:(固然剛進入界面就初始化也能夠,後面鏈接服務器以後再初始化也能夠,順序都行)

public void initPlayView(GLSurfaceView glSurfaceView) {
        VideoRendererGui.setView(glSurfaceView, (Runnable)null);
        this.isVideoRendererGuiSet = true;
    }
複製代碼

這一步就是要把glSurfaceView添加VideoRendererGui中,做爲要顯示的界面。session

三、登陸到視頻服務器,這一步其實應該是最開始的(地二、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;
        }
    }
複製代碼

四、登陸的時候,設置一下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回調收到你發送的本地流)

六、當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/…

注:整個項目中包含了上面的視頻實現代碼(項目中的功能比較多)

相關文章
相關標籤/搜索