WebRTC简介
WebRTC 是一种视频聊天和会议开发技术。它允许您在移动设备和浏览器之间创建点对点连接以传输媒体流。你可以在我们关于WebRTC的文章中找到更多关于它的工作原理及其一般原则的详细信息。
2种方式在Android上与WebRTC实现视频通信
- 最简单和最快的选择是使用众多商业项目之一,例如Twilio或LiveSwitch。它们为各种平台提供了自己的 SDK,并实现了开箱即用的功能,但它们也有缺点。他们是付费的,功能有限:你只能做他们有的功能,而不是你能想到的任何功能。
- 另一种选择是使用现有库之一。这种方法需要更多代码,但会为您节省资金并在功能实现方面为您提供更大的灵活性。在本文中,我们将研究第二个选项并使用https://webrtc.github.io/webrtc-org/native-code/android/作为我们的库。
创建连接
创建 WebRTC 连接包括两个步骤:
- 建立逻辑连接 - 设备必须就数据格式、编解码器等达成一致。
- 建立物理连接 - 设备必须知道彼此的地址
首先,请注意,在连接开始时,为了在设备之间交换数据,使用了信令机制。信令机制可以是任何用于传输数据的通道,例如套接字。
假设我们要在两个设备之间建立视频连接。为此,我们需要在它们之间建立逻辑连接。
逻辑连接
使用会话描述协议 (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()
)
}
反过来,另一个对等方:
- 还创建一个 PeerConnection 对象。
- 使用信号机制,接收到第一个peer中毒的SDP-offer并存储在自己
- 形成一个 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
网友评论