Android的WebRTC入门——回环点对点视频通话


点对点视频通话风靡一时,现在每个应用都内置了音频/视频通话。本系列教程旨在探索以简单易懂的方式为初学者介绍此类功能。

本教程系列大致基于Codelabs for WebRTC

本文主要是基于上述代码实验(Codelabs)的第 2 步 ,我们使用WebRTC中的PeerConnection在同一设备中的两个对等体之间传输音频和视频数据。

这是“ Android 版 WebRTC 入门 ”系列的 第 3 部分 ,如果您是第一次看到此篇文章,请确保在继续阅读这一部分之前,您已经阅读了本系列的前几部分。

第 1 部分: WebRTC 简介
第 2 部分: PeerConnection 简介
第 3 部分: 点对点视频通话 — 回环(本文)
第 4 部分: 使用 socket.io 进行点对点视频通话

让我们想象以下情景:一位勇敢的先生想给一位女士打电话(当然,打电话的目的是求婚!)

他计划使用一款具有强大的视频和音频通话功能的应用程序。因为他想看到他在求婚时对方的反应(哦~)。以下步骤发生在对等连接的两端。

  • 最初,我们的应用程序创建了一个对等连接和一个SDP指令。此指令包含调用对等方的数据,并用于识别对等方的编解码器和其他实体。

  • 这个指令之后被存储在呼叫方的 "本地描述 "中,然后通过一些信号介质发送到被呼叫方那里(通常,大多数系统使用WebSocket作为信令介质。它可能会根据您的使用方法和要求而有所不同)。

  • 一旦我们在被呼叫方的应用程序收到指令,它就知道有一个呼叫需要建立。从而将“指令”存储为“远程描述”,并创建相应的“应答”SDP。

  • 这个应答SDP类似于呼叫方的指令 SDP,它将指出该对等体的具体细节。

  • 被呼叫方的应用程序将这个 "应答SDP “存储为"本地描述”,然后通过信号通道将其发送给呼叫方。

  • 呼叫方收到应答并将它作为 "远程描述 "储存起来。

  • 然后呼叫方和被呼叫方通过信号通道传输与他们有关的 Ice Candidates。收到这些candidates后,对等方会将这些candidates添加到它们的 PeerConnection 实例中。

  • 一旦Ice Candidates的传输完成,对等方就清楚地知道如何在他们之间传输媒体数据。媒体数据传输通过RTP进行,由WebRTC框架负责。

如果您看过WebRTC codelabs的 第 2 步,您可能会注意到它们在呼叫方和被呼叫方之间创建了一个循环。

单个设备既是本地对等体,又是远程对等体,从而完成指令和应答的部分。虽然它没有任何实际用途,但最好通过这一步来深入了解它的工作原理。

我们一起看看下面的代码,了解循环工作是如何实现的。


public void start() {
    //Initialize PeerConnectionFactory globals.
    //Params are context, initAudio,initVideo and videoCodecHwAcceleration
    PeerConnectionFactory.initializeAndroidGlobals(this, true, true, true);

    //Create a new PeerConnectionFactory instance.
    PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
    peerConnectionFactory = new PeerConnectionFactory(options);

    //Now create a VideoCapturer instance. Callback methods are there if you want to do something! Duh!
    VideoCapturer videoCapturerAndroid = getVideoCapturer(new CustomCameraEventsHandler());

    //Create MediaConstraints - Will be useful for specifying video and audio constraints.
    audioConstraints = new MediaConstraints();
    videoConstraints = new MediaConstraints();

    //Create a VideoSource instance
    videoSource = peerConnectionFactory.createVideoSource(videoCapturerAndroid, videoConstraints);
    localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);

    //create an AudioSource instance
    audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
    localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
    localVideoView.setVisibility(View.VISIBLE);

    //create a videoRenderer based on SurfaceViewRenderer instance
    localRenderer = new VideoRenderer(localVideoView);
    // And finally, with our VideoRenderer ready, we
    // can add our renderer to the VideoTrack.
    localVideoTrack.addRenderer(localRenderer);
}

private void call() {
    //we already have video and audio tracks. Now create peerconnections
    List<PeerConnection.IceServer> iceServers = new ArrayList<>();

    //create sdpConstraints
    sdpConstraints = new MediaConstraints();
    sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true"));
    sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true"));

    //creating localPeer
    localPeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("localPeerCreation") {
        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            super.onIceCandidate(iceCandidate);
            onIceCandidateReceived(localPeer, iceCandidate);
        }
    });

    //creating remotePeer
    remotePeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("remotePeerCreation") {

        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            super.onIceCandidate(iceCandidate);
            onIceCandidateReceived(remotePeer, iceCandidate);
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
            super.onAddStream(mediaStream);
            gotRemoteStream(mediaStream);
        }
    });

    //creating local mediastream
    MediaStream stream = peerConnectionFactory.createLocalMediaStream("102");
    stream.addTrack(localAudioTrack);
    stream.addTrack(localVideoTrack);
    localPeer.addStream(stream);

    //creating Offer
    localPeer.createOffer(new CustomSdpObserver("localCreateOffer"){
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
            //we have localOffer. Set it as local desc for localpeer and remote desc for remote peer.
            //try to create answer from the remote peer.
            super.onCreateSuccess(sessionDescription);
            localPeer.setLocalDescription(new CustomSdpObserver("localSetLocalDesc"), sessionDescription);
            remotePeer.setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription);
            remotePeer.createAnswer(new CustomSdpObserver("remoteCreateOffer") {
                @Override
                public void onCreateSuccess(SessionDescription sessionDescription) {
                    //remote answer generated. Now set it as local desc for remote peer and remote desc for local peer.
                    super.onCreateSuccess(sessionDescription);
                    remotePeer.setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription);
                    localPeer.setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription);
                }
            },new MediaConstraints());
        }
    },sdpConstraints);
}


private void hangup() {
    localPeer.close();
    remotePeer.close();
    localPeer = null;
    remotePeer = null;
}

private void gotRemoteStream(MediaStream stream) {
    //we have remote video stream. add to the renderer.
    final VideoTrack videoTrack = stream.videoTracks.getFirst();
    AudioTrack audioTrack = stream.audioTracks.getFirst();
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            try {
                remoteRenderer = new VideoRenderer(remoteVideoView);
                remoteVideoView.setVisibility(View.VISIBLE);
                videoTrack.addRenderer(remoteRenderer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

public void onIceCandidateReceived(PeerConnection peer, IceCandidate iceCandidate) {
    //we have received ice candidate. We can set it to the other peer.
    if (peer == localPeer) {
        remotePeer.addIceCandidate(iceCandidate);
    } else {
        localPeer.addIceCandidate(iceCandidate);
    }
}

如果你看一下上面的代码,我们能总结出3个方法。

start() 本质上是创建本地音频和视频源,并将它们添加到SurfaceViewRenderer(这是WebRTC提供的,这样我们就可以直接在这个视图上绘制我们的视频帧)。

hangup() 只是一段简单的代码,用于清除所有PeerConnection实例。

call() 的过程中会发生很多有趣的事儿。

  • 我们创建two 对等体连接实例(一个用于本地对等体,另一个用于远程对等体)。一旦这些对等体被创建,我们让本地对等体创建一个Offer,该指令被设置为其本地描述,也是远程对等体的远程描述。

  • 然后我们让远程对等体做出相应的answer ,该应答被设置为其本地描述和本地对等体的远程描述。

我们还有onIceCandidateReceived() 方法,它的工作是将从一个对等方接收到的Ice candidates设置给另一个对等方。

你可以看看 Git 存储库的的Step-2文件夹,了解Loopback peerconnection的完整工作代码。

现在一切都很顺利。我们的应用程序将能够让您看到自己的脸,就像我们在第一部分做的那样。只是这次我们的应用程序是通过对等连接来显示脸部。

我们了解了基本的工作原理。我们有两个对等体,它们与candidates一起传输指令和应答SDP,现在它们都可以传输数据。但这并不是真实的情况。实际上人们会互相呼叫,而不是自己呼叫自己。将此解决方案扩展到实际用例很容易。我们只需要一个媒介来传输SDP和一些STUN和TURN设置。

这些内容对于单个帖子来说已经足够充实了。 我们很快就会看到(希望如此!)成品,届时你将拥有一个能够工作的点对点视频通话应用程序。

一定要按下绿色的按键来表达支持哦!有关更多 droid-y 帖子,请关注 Adventurous Android

原文作者:Vivek Chanddru
原文链接:https://vivekc.xyz/peer-to-peer-video-calling-webrtc-for-android-4132fd0ac54

推荐阅读
作者信息
AgoraTechnicalTeam
TA 暂未填写个人简介
文章
175
相关专栏
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。