Android 版 WebRTC 入门 — 轻松开发视频通话APP!

因为人们都逐渐倾向使用语音和视频通话,所以WebRTC 发展十分迅速并走向大众化,但在 Android 中实现音视频通话却很困难,本教程的内容是希望帮助每个开发者都能轻松了解在Android中开发此功能的过程。

Google 发现有很多 Android 开发者在努力使用 WebRTC(可能这系列文章让他们意识到WebRTC的重要性了?他们已经发布了用于 WebRTC 开发的新gradle 依赖项。注意!这些依赖项仅用于开发 目的!

本文将使用 WebRTC 的最新依赖来开发端到端的 Peer-to-Peer 视频通话APP。

经过一段时间的断更(本人表示抱歉:pensive:),我来更新这个系列的最后一部分了。(欢呼!)

这是“ Android 版 WebRTC 入门 ”系列的第 4 部分 ,如果你第一次阅读本系列文章,务必要确保已阅读本系列的前几章,然后再继续阅读本部分。

第 1 部分: WebRTC 简介

第 2 部分: PeerConnection 简介

第 3 部分: 点对点视频通话 — 环回

第 4 部分: 使用 socket.io 进行点对点视频通话(本文)

倒叙

还记得那个想向他的女朋友求婚的书呆子吗?在学完上一篇教程后,他已经能够自己创建一个APP并开始自言自语(如果你还没有,请参阅此处了解 loopback 调用)。但太糟糕了,自言自语并不会帮助他收获爱情。他想达到如下图所示的效果。

爱不是简单的事。WebRTC 也是如此。当你终于得到它时,它虽然很单调但很甜蜜!

实际的转移实际上是这样的。

  • 用户端必须通过某种方式登录到信令服务器。这有助于服务器识别呼叫的正确接收者。这使用 (1,2,3 和 4) 表示

  • 登录后,我们这个书呆子创建指令,存储副本(本地描述)并将其发送到充当快递员的服务器并将其交付给他心仪的女孩。(5,6,7 和 8)

  • 女孩现在打开求婚信息,将其存储到她的收藏中(远程描述),生成对求婚指令的响应(答案),将其存储在本地并通过同一个快递员将其发送给男孩 (9-13)

  • 男孩现在存储从她那里收到的回复 (14)

  • 两人之间进行谈判(Ice Candidates 谈判)(15-20)

  • 一旦连接上,只要他们愿意就可以畅所欲言!这可能通过他们之间的直接 P2P 连接或通过 TURN 服务器发生,如果有人在防火墙后面(听起来更像是女生的巨人父亲!)

随时待命

首先,我们需要一个服务器设置并与 TURN 和 STUN 一起运行。如果你是新手,STUN 通常是查找设备的公共 IP而TURN 作为中继服务器,以防两端用户之间无法进行 P2P。如果你想了解更多信息,可以参考本系列的第 2 部分,该部分包含理论!

对于这个演示,我们将使用Xirsys提供的平台来满足我们的 STUN 和 TURN 需求。我免费订阅了他们的网站并使用了他们的免费套餐。如果你想将其扩展到生产,你可能必须选择付费订阅或拥有自己的设置。

我们将使用 Google 的代码实验室文章来设置我们的信令服务器。本文更侧重于 Android 开发人员范围,在服务器端没有做太多工作。因此,我们将使用与代码实验室演示中几乎相同的服务器。

按照此处的链接了解如何为本地开发配置 socket.io。你可能不想使用此服务器代码进行生产,因为它有很多安全漏洞。(那里的代码来自 socket.io 1.2 版——为了演示,我们将使用相同的版本)

'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var https = require('https');
var socketIO = require('socket.io');
var fs = require("fs");
var options = {
	key: fs.readFileSync('key.pem'),
	cert: fs.readFileSync('cert.pem')
};

var fileServer = new(nodeStatic.Server)();
var app = https.createServer(options,function(req, res) {
  fileServer.serve(req, res);

}).listen(1794);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // for a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);  
});

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var numClients = io.sockets.sockets.length;      
log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 1) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 2) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max 5 clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

  socket.on('bye', function(){
    console.log('received bye');
     
});

});

如需完整的节点服务器,可查看GitHub 存储库。

Android

谈到 Android 部分,如果你到目前为止一直在关注该系列,你可能会发现这很容易。在上一部分中,我们在同一个活动类中创建了本地和远程节点,并尝试将音频和视频从一个节点传输到另一个节点。我们从本地两端用户获得了求婚信息,并将其设置为远程对等方实例。

当我们处理远程调用时,我们只关心我们自己的本地对等方。相比于管理远程对等点,我们更需要通过信令通道发送连接所需的数据(SDP 和 ICE 候选)。

让我们来看看呼叫处理和信令的代码。

class MainActivityKotlin : AppCompatActivity(), View.OnClickListener, SignallingClientKotlin.SignalingInterface {
    private val rootEglBase by lazy { EglBase.create() }

    private val peerConnectionFactory: PeerConnectionFactory by lazy {
        //Initialize PeerConnectionFactory globals.
        val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(this)
                .setEnableVideoHwAcceleration(true)
                .createInitializationOptions()
        PeerConnectionFactory.initialize(initializationOptions)

        //Create a new PeerConnectionFactory instance - using Hardware encoder and decoder.
        val options = PeerConnectionFactory.Options()
        val defaultVideoEncoderFactory = DefaultVideoEncoderFactory(
                rootEglBase.eglBaseContext, /* enableIntelVp8Encoder */true, /* enableH264HighProfile */true)
        val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(rootEglBase.eglBaseContext)
        PeerConnectionFactory(options, defaultVideoEncoderFactory, defaultVideoDecoderFactory)
    }

    private var sdpConstraints: MediaConstraints? = null
    private var localVideoTrack: VideoTrack? = null
    private var localAudioTrack: AudioTrack? = null


    private var localVideoView: SurfaceViewRenderer? = null
    private var remoteVideoView: SurfaceViewRenderer? = null

    private var hangup: Button? = null
    private var localPeer: PeerConnection? = null

    private var gotUserMedia: Boolean = false
    private var peerIceServers: MutableList<PeerConnection.IceServer> = ArrayList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initViews()
        initVideos()
        getIceServers()
        SignallingClientKotlin.init(this)
        start()
    }


    private fun initViews() {
        hangup = findViewById(R.id.end_call)
        localVideoView = findViewById(R.id.local_gl_surface_view)
        remoteVideoView = findViewById(R.id.remote_gl_surface_view)
        hangup?.setOnClickListener(this)
    }

    private fun initVideos() {
        localVideoView?.init(rootEglBase.eglBaseContext, null)
        remoteVideoView?.init(rootEglBase.eglBaseContext, null)
        localVideoView?.setZOrderMediaOverlay(true)
        remoteVideoView?.setZOrderMediaOverlay(true)
    }

    private fun getIceServers() {
        //get Ice servers using xirsys
        Utils.getInstance().retrofitInstance.iceCandidates.enqueue(object : Callback<TurnServerPojo> {
            override fun onResponse(call: Call<TurnServerPojo>, response: Response<TurnServerPojo>) {
                var iceServers: List<IceServer> = ArrayList()
                val body = response.body()
                if (body != null) {
                    iceServers = body.iceServerList.iceServers
                }
                for (iceServer in iceServers) {
                    if (iceServer.credential == null) {
                        val peerIceServer = PeerConnection.IceServer.builder(iceServer.url).createIceServer()
                        peerIceServers.add(peerIceServer)
                    } else {
                        val peerIceServer = PeerConnection.IceServer.builder(iceServer.url)
                                .setUsername(iceServer.username)
                                .setPassword(iceServer.credential)
                                .createIceServer()
                        peerIceServers.add(peerIceServer)
                    }
                }
                Log.d("onApiResponse", "IceServers\n" + iceServers.toString())
            }

            override fun onFailure(call: Call<TurnServerPojo>, t: Throwable) {
                t.printStackTrace()
            }
        })
    }


    private fun start() {


        //Now create a VideoCapturer instance.
        val videoCapturerAndroid: VideoCapturer? = createCameraCapturer(Camera1Enumerator(false))

        val audioConstraints = MediaConstraints()

        //Create MediaConstraints - Will be useful for specifying video and audio constraints.

        val videoSource: VideoSource

        //Create a VideoSource instance
        if (videoCapturerAndroid != null) {
            videoSource = peerConnectionFactory.createVideoSource(videoCapturerAndroid)
            localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource)
        }

        //create an AudioSource instance
        val audioSource = peerConnectionFactory.createAudioSource(audioConstraints)
        localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource)


        videoCapturerAndroid?.startCapture(1024, 720, 30)
        localVideoView?.visibility = View.VISIBLE
        //create a videoRenderer based on SurfaceViewRenderer instance
        val localRenderer = VideoRenderer(localVideoView)
        // And finally, with our VideoRenderer ready, we
        // can add our renderer to the VideoTrack.
        localVideoTrack?.addRenderer(localRenderer)

        localVideoView?.setMirror(true)
        remoteVideoView?.setMirror(true)

        gotUserMedia = true
        if (SignallingClientKotlin.isInitiator) {
            onTryToStart()
        }
    }


    /**
     * This method will be called directly by the app when it is the initiator and has got the local media
     * or when the remote peer sends a message through socket that it is ready to transmit AV data
     */
    override fun onTryToStart() {
        runOnUiThread {
            if (!SignallingClientKotlin.isStarted && localVideoTrack != null && SignallingClientKotlin.isChannelReady) {
                createPeerConnection()
                SignallingClientKotlin.isStarted = true
                if (SignallingClientKotlin.isInitiator) {
                    doCall()
                }
            }
        }
    }


    /**
     * Creating the local peerconnection instance
     */
    private fun createPeerConnection() {
        val rtcConfig = PeerConnection.RTCConfiguration(peerIceServers)
        // TCP candidates are only useful when connecting to a server that supports
        // ICE-TCP.
        rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED
        rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
        rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
        rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
        // Use ECDSA encryption.
        rtcConfig.keyType = PeerConnection.KeyType.ECDSA
        localPeer = peerConnectionFactory.createPeerConnection(rtcConfig, object : CustomPeerConnectionObserver("localPeerCreation") {
            override fun onIceCandidate(iceCandidate: IceCandidate) {
                super.onIceCandidate(iceCandidate)
                onIceCandidateReceived(iceCandidate)
            }

            override fun onAddStream(mediaStream: MediaStream) {
                showToast("Received Remote stream")
                super.onAddStream(mediaStream)
                gotRemoteStream(mediaStream)
            }
        })

        addStreamToLocalPeer()
    }

    /**
     * Adding the stream to the localpeer
     */
    private fun addStreamToLocalPeer() {
        //creating local mediastream
        val stream = peerConnectionFactory.createLocalMediaStream("102")
        stream.addTrack(localAudioTrack)
        stream.addTrack(localVideoTrack)
        localPeer!!.addStream(stream)
    }

    /**
     * This method is called when the app is initiator - We generate the offer and send it over through socket
     * to remote peer
     */
    private fun doCall() {
        localPeer!!.createOffer(object : CustomSdpObserver("localCreateOffer") {
            override fun onCreateSuccess(sessionDescription: SessionDescription) {
                super.onCreateSuccess(sessionDescription)
                localPeer!!.setLocalDescription(CustomSdpObserver("localSetLocalDesc"), sessionDescription)
                Log.d("onCreateSuccess", "SignallingClient emit ")
                SignallingClientKotlin.emitMessage(sessionDescription)
            }
        }, sdpConstraints)
    }

    /**
     * Received remote peer's media stream. we will get the first video track and render it
     */
    private fun gotRemoteStream(stream: MediaStream) {
        //we have remote video stream. add to the renderer.
        val videoTrack = stream.videoTracks[0]
        runOnUiThread {
            try {
                val remoteRenderer = VideoRenderer(remoteVideoView)
                remoteVideoView?.visibility = View.VISIBLE
                videoTrack.addRenderer(remoteRenderer)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

    }


    /**
     * Received local ice candidate. Send it to remote peer through signalling for negotiation
     */
    fun onIceCandidateReceived(iceCandidate: IceCandidate) {
        //we have received ice candidate. We can set it to the other peer.
        SignallingClientKotlin.emitIceCandidate(iceCandidate)
    }

    /**
     * SignallingCallback - called when the room is created - i.e. you are the initiator
     */
    override fun onCreatedRoom() {
        showToast("You created the room " + gotUserMedia)
        if (gotUserMedia) {
            SignallingClientKotlin.emitMessage("got user media")
        }
    }

    /**
     * SignallingCallback - called when you join the room - you are a participant
     */
    override fun onJoinedRoom() {
        showToast("You joined the room " + gotUserMedia)
        if (gotUserMedia) {
            SignallingClientKotlin.emitMessage("got user media")
        }
    }

    override fun onNewPeerJoined() {
        showToast("Remote Peer Joined")
    }

    override fun onRemoteHangUp(msg: String) {
        showToast("Remote Peer hungup")
        runOnUiThread({ this.hangup() })
    }

    /**
     * SignallingCallback - Called when remote peer sends offer
     */
    override fun onOfferReceived(data: JSONObject) {
        showToast("Received Offer")
        runOnUiThread {
            if (!SignallingClientKotlin.isInitiator && !SignallingClientKotlin.isStarted) {
                onTryToStart()
            }
            try {
                localPeer!!.setRemoteDescription(CustomSdpObserver("localSetRemote"), SessionDescription(SessionDescription.Type.OFFER, data.getString("sdp")))
                doAnswer()
                updateVideoViews(true)
            } catch (e: JSONException) {
                e.printStackTrace()
            }
        }
    }

    private fun doAnswer() {
        localPeer!!.createAnswer(object : CustomSdpObserver("localCreateAns") {
            override fun onCreateSuccess(sessionDescription: SessionDescription) {
                super.onCreateSuccess(sessionDescription)
                localPeer!!.setLocalDescription(CustomSdpObserver("localSetLocal"), sessionDescription)
                SignallingClientKotlin.emitMessage(sessionDescription)
            }
        }, MediaConstraints())
    }

    /**
     * SignallingCallback - Called when remote peer sends answer to your offer
     */

    override fun onAnswerReceived(data: JSONObject) {
        showToast("Received Answer")
        try {
            localPeer!!.setRemoteDescription(CustomSdpObserver("localSetRemote"), SessionDescription(SessionDescription.Type.fromCanonicalForm(data.getString("type").toLowerCase()), data.getString("sdp")))
            updateVideoViews(true)
        } catch (e: JSONException) {
            e.printStackTrace()
        }

    }

    /**
     * Remote IceCandidate received
     */
    override fun onIceCandidateReceived(data: JSONObject) {
        try {
            localPeer!!.addIceCandidate(IceCandidate(data.getString("id"), data.getInt("label"), data.getString("candidate")))
        } catch (e: JSONException) {
            e.printStackTrace()
        }

    }

    private fun updateVideoViews(remoteVisible: Boolean) {
        runOnUiThread {
            var params = localVideoView?.layoutParams
            params?.let {
                if (remoteVisible) {
                    it.height = dpToPx(100)
                    it.width = dpToPx(100)
                } else {
                    params = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                }
                localVideoView?.layoutParams = params
            }
        }
    }

    /**
     * Closing up - normal hangup and app destroye
     */

    override fun onClick(v: View) {
        when (v.id) {
            R.id.end_call -> {
                hangup()
            }
        }
    }

    private fun hangup() {
        try {
            localPeer!!.close()
            localPeer = null
            SignallingClientKotlin.close()
            updateVideoViews(false)
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    override fun onDestroy() {
        SignallingClientKotlin.close()
        super.onDestroy()
    }

    /**
     * Util Methods
     */
    private fun dpToPx(dp: Int): Int {
        val displayMetrics = resources.displayMetrics
        return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT))
    }

    private fun showToast(msg: String) {
        runOnUiThread { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() }
    }

    private fun createCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? {
        val deviceNames = enumerator.deviceNames
        //find the front facing camera and return it.
        deviceNames
                .filter { enumerator.isFrontFacing(it) }
                .mapNotNull { enumerator.createCapturer(it, null) }
                .forEach { return it }

        return null
    }
}
internal class SignallingClientKotlin {


    internal interface SignalingInterface {
        fun onRemoteHangUp(msg: String)

        fun onOfferReceived(data: JSONObject)

        fun onAnswerReceived(data: JSONObject)

        fun onIceCandidateReceived(data: JSONObject)

        fun onTryToStart()

        fun onCreatedRoom()

        fun onJoinedRoom()

        fun onNewPeerJoined()
    }

    companion object {
        private var roomName: String? = null

        init {
            if (roomName == null) {
                roomName = "some_room_name"
            }
        }

        private var socket: Socket? = null
        var isChannelReady = false
        var isInitiator = false
        var isStarted = false
        private var callback: SignalingInterface? = null

        //This piece of code should not go into production?
        //This will help in cases where the node server is running in non-https server and you want to ignore the warnings
        private val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
            override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
                return arrayOf()
            }

            @SuppressLint("TrustAllX509TrustManager")
            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
            }

            @SuppressLint("TrustAllX509TrustManager")
            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
            }
        })


        fun init(signalingInterface: SignalingInterface) {
            this.callback = signalingInterface
            try {
                val sslcontext = SSLContext.getInstance("TLS")
                sslcontext.init(null, trustAllCerts, null)
                IO.setDefaultHostnameVerifier { _, _ -> true }
                IO.setDefaultSSLContext(sslcontext)
                //set the socket.io url here
                socket = IO.socket("your_socket_io_instance_url_with_port")
                socket?.connect()
                Log.d("SignallingClient", "init() called")

                roomName?.let { emitInitStatement(it) }


                //room created event.
                socket?.on("created") { args ->
                    Log.d("SignallingClient", "created call() called with: args = [" + Arrays.toString(args) + "]")
                    isInitiator = true
                    callback?.onCreatedRoom()
                }

                //room is full event
                socket?.on("full") { args -> Log.d("SignallingClient", "full call() called with: args = [" + Arrays.toString(args) + "]") }

                //peer joined event
                socket?.on("join") { args ->
                    Log.d("SignallingClient", "join call() called with: args = [" + Arrays.toString(args) + "]")
                    isChannelReady = true
                    callback?.onNewPeerJoined()
                }

                //when you joined a chat room successfully
                socket?.on("joined") { args ->
                    Log.d("SignallingClient", "joined call() called with: args = [" + Arrays.toString(args) + "]")
                    isChannelReady = true
                    callback?.onJoinedRoom()
                }

                //log event
                socket?.on("log") { args -> Log.d("SignallingClient", "log call() called with: args = [" + Arrays.toString(args) + "]") }

                //bye event
                socket?.on("bye") { args -> callback?.onRemoteHangUp(args[0] as String) }

                //messages - SDP and ICE candidates are transferred through this
                socket?.on("message") { args ->
                    Log.d("SignallingClient", "message call() called with: args = [" + Arrays.toString(args) + "]")
                    when (args[0]) {
                        is String -> {
                            Log.d("SignallingClient", "String received :: " + args[0])
                            val data = args[0] as String
                            if (data.equals("got user media", ignoreCase = true)) {
                                callback?.onTryToStart()
                            }
                            if (data.equals("bye", ignoreCase = true)) {
                                callback?.onRemoteHangUp(data)
                            }
                        }
                        is JSONObject -> try {
                            val data = args[0] as JSONObject
                            Log.d("SignallingClient", "Json Received :: " + data.toString())
                            val type = data.getString("type")
                            if (type.equals("offer", ignoreCase = true)) {
                                callback?.onOfferReceived(data)
                            } else if (type.equals("answer", ignoreCase = true) && isStarted) {
                                callback?.onAnswerReceived(data)
                            } else if (type.equals("candidate", ignoreCase = true) && isStarted) {
                                callback?.onIceCandidateReceived(data)
                            }

                        } catch (e: JSONException) {
                            e.printStackTrace()
                        }
                    }
                }
            } catch (e: URISyntaxException) {
                e.printStackTrace()
            } catch (e: NoSuchAlgorithmException) {
                e.printStackTrace()
            } catch (e: KeyManagementException) {
                e.printStackTrace()
            }

        }

        private fun emitInitStatement(message: String) {
            Log.d("SignallingClient", "emitInitStatement() called with: event = [create or join], message = [$message]")
            socket?.emit("create or join", message)
        }

        fun emitMessage(message: String) {
            Log.d("SignallingClient", "emitMessage() called with: message = [$message]")
            socket?.emit("message", message)
        }

        fun emitMessage(message: SessionDescription) {
            try {
                Log.d("SignallingClient", "emitMessage() called with: message = [$message]")
                val obj = JSONObject()
                obj.put("type", message.type.canonicalForm())
                obj.put("sdp", message.description)
                Log.d("emitMessage", obj.toString())
                socket?.emit("message", obj)
                Log.d("vivek1794", obj.toString())
            } catch (e: JSONException) {
                e.printStackTrace()
            }

        }


        fun emitIceCandidate(iceCandidate: IceCandidate) {
            try {
                val jsonObject = JSONObject()
                jsonObject.put("type", "candidate")
                jsonObject.put("label", iceCandidate.sdpMLineIndex)
                jsonObject.put("id", iceCandidate.sdpMid)
                jsonObject.put("candidate", iceCandidate.sdp)
                socket?.emit("message", jsonObject)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        fun close() {
            socket?.emit("bye", roomName)
            socket?.disconnect()
            socket?.close()
        }
    }
}

这有很多代码。但如果你把它分成几部分,并用第 2 部分的代码来引用它,就会很有意义。注释应该可以帮你找到通过代码的方式。(如果你仍然使用 JAVA,可查看 Github 存储库 — MainActivity.javaSignallingClient.java文件)

简而言之,我们所做的就是尝试建立一个到socket.io web socket的连接,并且当连接被创建时,我们向服务器发送一个房间名称。如果尚未创建新房间,服务器将创建一个新房间,如果该房间已经存在并且有空余空间,则会把你添加到该房间。(为了演示目的,房间容量设置为 2)

如果你是创建者,则需要在其他参与者连接到会话后将信息发送给他们。然后第二个参与者将通过套接字接收它、创建他们的答案并通过套接字将其发送给发起者。

关闭

现在 SDP 和 ICE 候选对象通过套接字从一个参与者传输到另一个参与者,WebRTC 将发挥其魔力并在参与者之间传输音频和视频流。

我们的男孩现在有他的小周末项目,现在可以与他的女孩取得联系并说出他的想法:grimacing:(一切都很顺利,这甚至可能是这篇文章被推迟的原因)

这一步的代码是 在Github上。这使用了 WebRTC 的最新 gradle 依赖项,如果你使用的是旧版本的 WebRTC,它可能会对你的代码进行一些重大更改。要确保在你的APP中启用 Java 8 兼容性,并再次查看我们创建 PeerConnectionFactory 和 PeerConnection 实例的位置,因为那里发生了一些变化,它可能会破坏您的旧代码。

如果遇到任何问题,可以与我联系!

原文作者 Vivek Chanddru
原文链接 https://vivekc.xyz/getting-started-with-webrtc-part-4-de72b58ab31e

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