Android WebRTC完整入門教程02: 本地回環(huán)

上一篇: Android WebRTC完整入門教程01: 使用相機

在上一篇中完成了WebRTC最基本的使用--相機的使用. 這一篇將介紹WebRTC中最核心的概念PeerConnection?, 給同一手機中的前后攝像頭建立虛擬的連接, 相互傳輸畫面.

PeerConnection

PeerConnection也就是Peer-to-Peer connection(P2P), 就是兩個"人"的連接. 雙方分別創(chuàng)建PeerConnection對象, 然后向?qū)Ψ桨l(fā)送自己的網(wǎng)絡狀況ICE和多媒體編碼格式SDP(因為這時候連接還沒建立, 所以發(fā)送內(nèi)容是通過服務器完成的). 當雙方網(wǎng)絡和編碼格式協(xié)商好后, 連接就建立好了, 這時從PeerConnection中能獲取到對方的MediaStream數(shù)據(jù)流, 也就能播放對方的音視頻了.

ICE

Interactive Connectivity Establishment, 交互式連接建立. 其實是一個整合STUN和TURN的框架, 給它提供STUN和TURN服務器地址, 它會自動選擇優(yōu)先級高的進行NAT穿透.

SDP

Session Description Protocol: 會話描述協(xié)議. 發(fā)送方的叫Offer, 接受方的叫Answer, 除了名字外沒有區(qū)別. 就是一些文本描述本地的音視頻編碼和網(wǎng)絡地址等.

主要流程

A(local)和B(remote)代表兩個人, 初始化PeerConnectionFactory并分別創(chuàng)建PeerConnection?, 并向PeerConnection?添加本地媒體流.

  1. A創(chuàng)建Offer
  2. A保存Offer(set local description)
  3. A發(fā)送Offer給B
  4. B保存Offer(set remote description)
  5. B創(chuàng)建Answer
  6. B保存Answer(set local description)
  7. B發(fā)送Answer給A
  8. A保存Answer(set remote description)
  9. A發(fā)送Ice Candidates給B
  10. B發(fā)送Ice Candidates給A
  11. A,B收到對方的媒體流并播放


    建立PeerConnection.jpeg

    如上圖所示, 總共11步, 雖然步驟不少, 但其實并不復雜, 雙方基本是對稱的. 主要代碼如下.

準備步驟

主要是初始化PeerConnectionFactory和使用相機, 在上一篇已介紹過.

public class MainActivity extends AppCompatActivity {

    PeerConnectionFactory peerConnectionFactory;
    PeerConnection peerConnectionLocal;
    PeerConnection peerConnectionRemote;
    SurfaceViewRenderer localView;
    SurfaceViewRenderer remoteView;
    MediaStream mediaStreamLocal;
    MediaStream mediaStreamRemote;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext();

        // create PeerConnectionFactory
        PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions
                .builder(this)
                .createInitializationOptions());
        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
        DefaultVideoEncoderFactory defaultVideoEncoderFactory =
                new DefaultVideoEncoderFactory(eglBaseContext, true, true);
        DefaultVideoDecoderFactory defaultVideoDecoderFactory =
                new DefaultVideoDecoderFactory(eglBaseContext);
        peerConnectionFactory = PeerConnectionFactory.builder()
                .setOptions(options)
                .setVideoEncoderFactory(defaultVideoEncoderFactory)
                .setVideoDecoderFactory(defaultVideoDecoderFactory)
                .createPeerConnectionFactory();

        SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
        // create VideoCapturer
        VideoCapturer videoCapturer = createCameraCapturer(true);
        VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
        videoCapturer.initialize(surfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
        videoCapturer.startCapture(480, 640, 30);

        localView = findViewById(R.id.localView);
        localView.setMirror(true);
        localView.init(eglBaseContext, null);

        // create VideoTrack
        VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
//        // display in localView
//        videoTrack.addSink(localView);




        SurfaceTextureHelper remoteSurfaceTextureHelper = SurfaceTextureHelper.create("RemoteCaptureThread", eglBaseContext);
        // create VideoCapturer
        VideoCapturer remoteVideoCapturer = createCameraCapturer(false);
        VideoSource remoteVideoSource = peerConnectionFactory.createVideoSource(remoteVideoCapturer.isScreencast());
        remoteVideoCapturer.initialize(remoteSurfaceTextureHelper, getApplicationContext(), remoteVideoSource.getCapturerObserver());
        remoteVideoCapturer.startCapture(480, 640, 30);

        remoteView = findViewById(R.id.remoteView);
        remoteView.setMirror(false);
        remoteView.init(eglBaseContext, null);

        // create VideoTrack
        VideoTrack remoteVideoTrack = peerConnectionFactory.createVideoTrack("102", remoteVideoSource);
//        // display in remoteView
//        remoteVideoTrack.addSink(remoteView);



        mediaStreamLocal = peerConnectionFactory.createLocalMediaStream("mediaStreamLocal");
        mediaStreamLocal.addTrack(videoTrack);

        mediaStreamRemote = peerConnectionFactory.createLocalMediaStream("mediaStreamRemote");
        mediaStreamRemote.addTrack(remoteVideoTrack);

        call(mediaStreamLocal, mediaStreamRemote);
    }
}

使用相機

對createCameraCapturer()方法略作修改, 傳入boolean參數(shù)就能分別獲取前后攝像頭.

 private VideoCapturer createCameraCapturer(boolean isFront) {
        Camera1Enumerator enumerator = new Camera1Enumerator(false);
        final String[] deviceNames = enumerator.getDeviceNames();

        // First, try to find front facing camera
        for (String deviceName : deviceNames) {
            if (isFront ? enumerator.isFrontFacing(deviceName) : enumerator.isBackFacing(deviceName)) {
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);

                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }

        return null;
    }

撥打

建立連接的兩人肯定有一個是撥打方, 另一個是接受方. 撥打方創(chuàng)建Offer發(fā)給接受方, 接收方收到后回復Answer.

    private void call(MediaStream localMediaStream, MediaStream remoteMediaStream) {
        List<PeerConnection.IceServer> iceServers = new ArrayList<>();
        peerConnectionLocal = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("localconnection") {
            @Override
            public void onIceCandidate(IceCandidate iceCandidate) {
                super.onIceCandidate(iceCandidate);
                peerConnectionRemote.addIceCandidate(iceCandidate);
            }

            @Override
            public void onAddStream(MediaStream mediaStream) {
                super.onAddStream(mediaStream);
                VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
                runOnUiThread(() -> {
                    remoteVideoTrack.addSink(localView);
                });
            }
        });

        peerConnectionRemote = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("remoteconnection") {
            @Override
            public void onIceCandidate(IceCandidate iceCandidate) {
                super.onIceCandidate(iceCandidate);
                peerConnectionLocal.addIceCandidate(iceCandidate);
            }

            @Override
            public void onAddStream(MediaStream mediaStream) {
                super.onAddStream(mediaStream);
                VideoTrack localVideoTrack = mediaStream.videoTracks.get(0);
                runOnUiThread(() -> {
                    localVideoTrack.addSink(remoteView);
                });
            }
        });

        peerConnectionLocal.addStream(localMediaStream);
        peerConnectionLocal.createOffer(new SdpAdapter("local offer sdp") {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                super.onCreateSuccess(sessionDescription);
                // todo crashed here
                peerConnectionLocal.setLocalDescription(new SdpAdapter("local set local"), sessionDescription);
                peerConnectionRemote.addStream(remoteMediaStream);
                peerConnectionRemote.setRemoteDescription(new SdpAdapter("remote set remote"), sessionDescription);
                peerConnectionRemote.createAnswer(new SdpAdapter("remote answer sdp") {
                    @Override
                    public void onCreateSuccess(SessionDescription sdp) {
                        super.onCreateSuccess(sdp);
                        peerConnectionRemote.setLocalDescription(new SdpAdapter("remote set local"), sdp);
                        peerConnectionLocal.setRemoteDescription(new SdpAdapter("local set remote"), sdp);
                    }
                }, new MediaConstraints());
            }
        }, new MediaConstraints());
    }
}

注意: 雖然這里沒有真正使用到網(wǎng)絡, 但是要添加網(wǎng)絡權限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

網(wǎng)上大部分本地回環(huán)(Loopback)的Demo都只用到一個攝像頭, 這里使用到同一個手機的前后攝像頭, 把它們當做兩個客戶端, 建立模擬連接, 發(fā)送媒體數(shù)據(jù). 這跟實際WebRTC工作流程非常接近了, 只有一點差別--這里的數(shù)據(jù)傳輸是內(nèi)存共享, 而實際是通過網(wǎng)絡發(fā)送.

附錄

本項目GitHub地址/step2loopback

下一篇: Android WebRTC完整入門教程03: 信令

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哪廓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子初烘,更是在濱河造成了極大的恐慌涡真,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肾筐,死亡現(xiàn)場離奇詭異,居然都是意外死亡东亦,警方通過查閱死者的電腦和手機典阵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門蹋半,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捻爷,你說我怎么就攤上這事√鹱希” “怎么了囚霸?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵拓型,是天一觀的道長劣挫。 經(jīng)常有香客問我压固,道長邓夕,這世上最難降的妖魔是什么点弯? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮熬芜,結果婚禮上,老公的妹妹穿的比我還像新娘鼓拧。我一直安慰自己,他們只是感情好季俩,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著店归,像睡著了一般酪我。 火紅的嫁衣襯著肌膚如雪娱节。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天肄满,我揣著相機與錄音稠歉,去河邊找鬼怒炸。 笑死阅羹,一個胖子當著我的面吹牛捏鱼,可吹牛的內(nèi)容都是我干的酪耕。 我是一名探鬼主播导梆,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迂烁!你這毒婦竟也來了看尼?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盟步,失蹤者是張志新(化名)和其女友劉穎藏斩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體却盘,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡狰域,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年窜觉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片北专。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡禀挫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拓颓,到底是詐尸還是另有隱情语婴,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布驶睦,位于F島的核電站砰左,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏场航。R本人自食惡果不足惜缠导,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望溉痢。 院中可真熱鬧僻造,春花似錦、人聲如沸孩饼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镀娶。三九已至立膛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梯码,已是汗流浹背宝泵。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留轩娶,地道東北人儿奶。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像罢坝,于是被迫代替她去往敵國和親廓握。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容