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

WebRTC简介

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

两种方式在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()
  )
}

反过来,对于另一个peer:

  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 的重要组成部分。它们有一个目的——在没有公共地址的设备之间建立连接。

扫描二维码关注公众号,回复: 14873215 查看本文章

Stun server

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

Turn server

在某些情况下,路由器可能具有“对称 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安全指南。

猜你喜欢

转载自blog.csdn.net/CJohn1994/article/details/127017891