美文网首页
如何在 Android 上创建视频聊天?WebRTC 初学者指南

如何在 Android 上创建视频聊天?WebRTC 初学者指南

作者: 小城哇哇 | 来源:发表于2022-08-11 11:42 被阅读0次

    WebRTC简介

    WebRTC 是一种视频聊天和会议开发技术。它允许您在移动设备和浏览器之间创建点对点连接以传输媒体流。你可以在我们关于WebRTC的文章中找到更多关于它的工作原理及其一般原则的详细信息。

    2种方式在Android上与WebRTC实现视频通信

    • 最简单和最快的选择是使用众多商业项目之一,例如TwilioLiveSwitch。它们为各种平台提供了自己的 SDK,并实现了开箱即用的功能,但它们也有缺点。他们是付费的,功能有限:你只能做他们有的功能,而不是你能想到的任何功能。
    • 另一种选择是使用现有库之一。这种方法需要更多代码,但会为您节省资金并在功能实现方面为您提供更大的灵活性。在本文中,我们将研究第二个选项并使用https://webrtc.github.io/webrtc-org/native-code/android/作为我们的库。

    创建连接

    创建 WebRTC 连接包括两个步骤:

    1. 建立逻辑连接 - 设备必须就数据格式、编解码器等达成一致。
    2. 建立物理连接 - 设备必须知道彼此的地址

    首先,请注意,在连接开始时,为了在设备之间交换数据,使用了信令机制。信令机制可以是任何用于传输数据的通道,例如套接字。

    假设我们要在两个设备之间建立视频连接。为此,我们需要在它们之间建立逻辑连接。

    逻辑连接

    使用会话描述协议 (SDP) 为这一对等方建立逻辑连接:

    创建一个 PeerConnection 对象。

    在 SDP 提议上形成一个对象,其中包含有关即将到来的会话的数据,并使用信令机制将其发送给对话者。

    val peerConnectionFactory: PeerConnectionFactory
    lateinit var peerConnection: PeerConnection
    
    fun createPeerConnection(iceServers: List<PeerConnection.IceServer>) {
      val rtcConfig = PeerConnection.RTCConfiguration(iceServers)
      peerConnection = peerConnectionFactory.createPeerConnection(
          rtcConfig,
          object : PeerConnection.Observer {
              ...
          }
      )!!
    }
    
    fun sendSdpOffer() {
      peerConnection.createOffer(
          object : SdpObserver {
              override fun onCreateSuccess(sdpOffer: SessionDescription) {
                  peerConnection.setLocalDescription(sdpObserver, sdpOffer)
                  signaling.sendSdpOffer(sdpOffer)
              }
    
              ...
    
          }, MediaConstraints()
      )
    }
    
    

    反过来,另一个对等方:

    1. 还创建一个 PeerConnection 对象。
    2. 使用信号机制,接收到第一个peer中毒的SDP-offer并存储在自己
    3. 形成一个 SDP-answer 并将其发回,同样使用信号机制
    fun onSdpOfferReceive(sdpOffer: SessionDescription) {// Saving the received SDP-offer
      peerConnection.setRemoteDescription(sdpObserver, sdpOffer)
      sendSdpAnswer()
    }
    
    // FOrming and sending SDP-answer
    fun sendSdpAnswer() {
      peerConnection.createAnswer(
          object : SdpObserver {
              override fun onCreateSuccess(sdpOffer: SessionDescription) {
                  peerConnection.setLocalDescription(sdpObserver, sdpOffer)
                  signaling.sendSdpAnswer(sdpOffer)
              }
               …
          }, MediaConstraints()
      )
    }
    
    

    第一个节点收到 SDP 应答后,保留它

    fun onSdpAnswerReceive(sdpAnswer: SessionDescription) {
      peerConnection.setRemoteDescription(sdpObserver, sdpAnswer)
      sendSdpAnswer()
    }
    
    

    成功交换 SessionDescription 对象后,认为逻辑连接已建立。

    物理连接

    我们现在需要在设备之间建立物理连接,这通常是一项非常重要的任务。通常,Internet 上的设备没有公共地址,因为它们位于路由器和防火墙后面。为了解决这个问题,WebRTC 使用了 ICE(交互式连接建立)技术。

    Stun 和 Turn 服务器是 ICE 的重要组成部分。它们有一个目的——在没有公共地址的设备之间建立连接。

    眩晕服务器

    设备向 Stun 服务器发出请求并接收其公共地址作为响应。然后,使用信号机制将其发送给对话者。在对话者执行相同操作后,设备会识别彼此的网络位置并准备好相互传输数据。

    转服务器

    在某些情况下,路由器可能具有“对称 NAT”限制。此限制不允许设备之间的直接连接。在这种情况下,使用 Turn 服务器。它充当中介,所有数据都通过它。在Mozilla 的 WebRTC 文档中阅读更多内容。

    正如我们所见,STUN 和 TURN 服务器在建立设备之间的物理连接方面发挥着重要作用。正是出于这个目的,我们在创建 PeerConnection 对象时,传递一个包含可用 ICE 服务器的列表。

    为了建立物理连接,一个对等点生成 ICE 候选对象 - 包含有关如何在网络上找到设备的信息的对象,并通过信令机制将它们发送给对等点

    lateinit var peerConnection: PeerConnection
    
    fun createPeerConnection(iceServers: List<PeerConnection.IceServer>) {
    
      val rtcConfig = PeerConnection.RTCConfiguration(iceServers)
    
      peerConnection = peerConnectionFactory.createPeerConnection(
          rtcConfig,
          object : PeerConnection.Observer {
              override fun onIceCandidate(iceCandidate: IceCandidate) {
                  signaling.sendIceCandidate(iceCandidate)
              }           …
          }
      )!!
    }
    
    

    然后第二个对等点通过信令机制接收第一个对等点的候选 ICE 并为自己保留它们。它还生成自己的 ICE 候选人并将其发回

    fun onIceCandidateReceive(iceCandidate: IceCandidate) {
      peerConnection.addIceCandidate(iceCandidate)
    }
    
    

    现在对等点已经交换了他们的地址,您可以开始发送和接收数据。

    接收数据

    该库在与对话者建立逻辑和物理连接后,调用 onAddTrack 标头并将包含对话者的 VideoTrack 和 AudioTrack 的 MediaStream 对象传入其中

    fun createPeerConnection(iceServers: List<PeerConnection.IceServer>) {
    
       val rtcConfig = PeerConnection.RTCConfiguration(iceServers)
    
       peerConnection = peerConnectionFactory.createPeerConnection(
           rtcConfig,
           object : PeerConnection.Observer {
    
               override fun onIceCandidate(iceCandidate: IceCandidate) { … }
    
               override fun onAddTrack(
                   rtpReceiver: RtpReceiver?,
                   mediaStreams: Array<out MediaStream>
               ) {
                   onTrackAdded(mediaStreams)
               }
               … 
           }
       )!!
    }
    
    

    接下来,我们必须从 MediaStream 中检索 VideoTrack 并将其显示在屏幕上。

    private fun onTrackAdded(mediaStreams: Array<out MediaStream>) {
       val videoTrack: VideoTrack? = mediaStreams.mapNotNull {                                                            
           it.videoTracks.firstOrNull() 
       }.firstOrNull()
    
       displayVideoTrack(videoTrack)
    
       … 
    }
    
    

    要显示 VideoTrack,您需要向它传递一个实现 VideoSink 接口的对象。为此,该库提供了 SurfaceViewRenderer 类。

    fun displayVideoTrack(videoTrack: VideoTrack?) {
       videoTrack?.addSink(binding.surfaceViewRenderer)
    }
    
    

    为了获得对话者的声音,我们不需要做任何额外的事情——图书馆为我们做了一切。但是,如果我们想要微调声音,我们可以获取一个 AudioTrack 对象并使用它来更改音频设置

    var audioTrack: AudioTrack? = null
    private fun onTrackAdded(mediaStreams: Array<out MediaStream>) {
       … 
    
       audioTrack = mediaStreams.mapNotNull { 
           it.audioTracks.firstOrNull() 
       }.firstOrNull()
    }
    
    

    例如,我们可以使对话者静音,如下所示:

    fun muteAudioTrack() {
       audioTrack.setEnabled(false)
    }
    
    

    发送数据

    从您的设备发送视频和音频也开始于创建 PeerConnection 对象并发送 ICE 候选对象。但与从对话者接收视频流时创建SDPOffer不同,在这种情况下,我们必须首先创建一个MediaStream对象,其中包括AudioTrack和VideoTrack。

    为了发送我们的音频和视频流,我们需要创建一个 PeerConnection 对象,然后使用信令机制来交换 IceCandidate 和 SDP 数据包。但不是从库中获取媒体流,我们必须从我们的设备获取媒体流并将其传递给库,以便将其传递给我们的对话者。

    fun createLocalConnection() {
    
       localPeerConnection = peerConnectionFactory.createPeerConnection(
           rtcConfig,
           object : PeerConnection.Observer {
                ...
           }
       )!!
    
       val localMediaStream = getLocalMediaStream()
       localPeerConnection.addStream(localMediaStream)
    
       localPeerConnection.createOffer(
           object : SdpObserver {
                ...
           }, MediaConstraints()
       )
    }
    
    

    现在我们需要创建一个 MediaStream 对象并将 AudioTrack 和 VideoTrack 对象传递给它

    val context: Context
    private fun getLocalMediaStream(): MediaStream? {
       val stream = peerConnectionFactory.createLocalMediaStream("user")
    
       val audioTrack = getLocalAudioTrack()
       stream.addTrack(audioTrack)
    
       val videoTrack = getLocalVideoTrack(context)
       stream.addTrack(videoTrack)
    
       return stream
    }
    
    

    接收音轨:

    private fun getLocalAudioTrack(): AudioTrack {
       val audioConstraints = MediaConstraints()
       val audioSource = peerConnectionFactory.createAudioSource(audioConstraints)
       return peerConnectionFactory.createAudioTrack("user_audio", audioSource)
    }
    
    

    接收 VideoTrack 稍微困难一点。首先,获取设备所有摄像头的列表。

    lateinit var capturer: CameraVideoCapturer
    
    private fun getLocalVideoTrack(context: Context): VideoTrack {
       val cameraEnumerator = Camera2Enumerator(context)
       val camera = cameraEnumerator.deviceNames.firstOrNull {
           cameraEnumerator.isFrontFacing(it)
       } ?: cameraEnumerator.deviceNames.first()
    
       ...
    
    }
    
    

    接下来,创建一个 CameraVideoCapturer 对象,该对象将捕获图像

    private fun getLocalVideoTrack(context: Context): VideoTrack {
    
       ...
    
       capturer = cameraEnumerator.createCapturer(camera, null)
       val surfaceTextureHelper = SurfaceTextureHelper.create(
           "CaptureThread",
           EglBase.create().eglBaseContext
       )
       val videoSource =
           peerConnectionFactory.createVideoSource(capturer.isScreencast ?: false)
       capturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
    
       ...
    
    }
    
    

    现在,拿到CameraVideoCapturer后,开始抓图,添加到MediaStream

    private fun getLocalMediaStream(): MediaStream? {
      ...
    
      val videoTrack = getLocalVideoTrack(context)
      stream.addTrack(videoTrack)
    
      return stream
    }
    
    private fun getLocalVideoTrack(context: Context): VideoTrack {
        ...
    
      capturer.startCapture(1024, 720, 30)
    
      return peerConnectionFactory.createVideoTrack("user0_video", videoSource)
    
    }
    
    

    在创建一个 MediaStream 并将其添加到 PeerConnection 之后,该库形成一个 SDP 提议,并且上述 SDP 数据包交换通过信令机制进行。当这个过程完成后,对话者将开始接收我们的视频流。恭喜,此时连接已建立。

    多对多

    我们已经考虑了一对一的连接。WebRTC 还允许您创建多对多连接。在最简单的形式中,这与一对一连接的方式完全相同。不同之处在于 PeerConnection 对象,以及 SDP 数据包和 ICE-candidate 交换,不是为每个参与者完成一次。这种方法有缺点:

    • 设备负载很重,因为它需要向每个对话者发送相同的数据流
    • 视频录制、转码等附加功能的实现是困难的甚至是不可能的

    在这种情况下,WebRTC 可以与负责上述任务的媒体服务器结合使用。客户端的流程与直接连接对话者设备的过程完全相同,但媒体流不会发送给所有参与者,而只会发送给媒体服务器。媒体服务器将其重新传输给其他参与者。

    结论

    我们考虑了在 Android 上创建 WebRTC 连接的最简单方法。如果看完后你还是不明白,那就把所有的步骤都再一遍一遍,自己尝试去实现——一旦你掌握了关键点,在实践中使用这个技术就不成问题了。如果您想了解更多关于这项技术的信息,请查看我们的Webrtc 安全指南。

    原文来自:https://forasoft.hashnode.dev/how-to-create-video-chat-on-android-webrtc-guide-for-beginners

    相关文章

      网友评论

          本文标题:如何在 Android 上创建视频聊天?WebRTC 初学者指南

          本文链接:https://www.haomeiwen.com/subject/vgwzwrtx.html