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

2022-08-11  本文已影响0人  小城哇哇

WebRTC简介

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

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

创建连接

创建 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

上一篇下一篇

猜你喜欢

热点阅读