1- WebRTC传输基本知识

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1- WebRTC传输基本知识相关的知识,希望对你有一定的参考价值。

参考技术A NAT穿越可以分为四种类型:完全锥型、地址限制型、端口限制型、对称型

内网打穿后生成公网地址和端口,任意外网用户访问都可以访问,没有限制。

内网主机IP和端口、 NAT穿越后生成公网IP和端口、要请求的主机IP,其它主机的ip地址不是内网主机要请求的地址会失败

在地址限制型的基础上,增加了端口限制,如果请求的主机返回的端口也不对,请求也不成功

NAT穿越后会生成多个IP地址和端口号, 请求的主机对应一个ip地址和端口,内外穿到外网会生成不同的ip地址和端口号给不同的主机

NAT穿越的原理

前提要在服务端部署STUN服务器,并且有两个IP地址和端口

目的:NAT穿越,主机访问STUN服务器,返回一个公网ip地址
stun是典型的客户端/服务器响应模式,客户端发送请求,服务端响应。
规范:
RFC3489:通过UDP进行NAT穿越
RFC5389:通过UDP、TCP进行NAT穿越
由于UDP会有失败的情况,RFC5489引入TCP。

stun 5389格式

0b00:表示是一个请求
0b01:表示一个指示
0b10:表示一个请求成功的响应
0b11:表示一个请求失败的响应
最右侧是c0,

网络中使用大端模式,左边的优先被接收到,右边的最后接收到。

其中USERNAME、PASSWORD最重要,用于STUN服务器验证用户合法性

属性的什么时候使用

N/A 不支持,O是可选的,M是服务端,C是客户端

turn使用的传输协议
turn client - turn server :UDP、TCP、TLS over TCP
TURN server to peer: UDP
在STUN无法接通时,这时就需要公网的服务器作为一个中继,对来往的数据进行转发。这个转发的协议就被定义为TURN。TURN和其他中继协议的不同之处在于,它允许客户端使用同一个中继地址(relay address)与多个不同的peer进行通信。

使用TURN协议的客户端必须能够通过中继地址和对等端进行通讯,并且能够得知每个peer的的IP地址和端口(确切地说,应该是peer的服务器反射地址)。
TURN协议被设计为ICE协议的一部分,relay地址会作为一个候选,由ICE在多个候选中进行评估,选取最合适的通讯地址。一般来说中继relay的优先级都是最低的。
TURN协议本身是STUN的一个拓展,因此绝大部分TURN报文都是STUN类型的,作为STUN的一个拓展,TURN增加了新的方法(method)和属性(attribute)。

在典型的情况下,TURN客户端连接到内网中,并且通过一个或者多个NAT到达公网,TURN服务器架设在公网中,不同的客户端以TURN服务器为中继和其他peer进行通信,如下图所示:

在上图中,左边的TURN Client是位于NAT后面的一个客户端(内网地址是10.1.1.2:49721),连接公网的TURN服务器(默认端口3478)后,
服务器会得到一个Client的反射地址(Reflexive Transport Address, 即NAT分配的公网IP和端口)192.0.2.1:7000,
此时Client会通过TURN命令创建或管理ALLOCATION,allocation是服务器上的一个数据结构,包含了中继地址的信息。
服务器随后会给Client分配一个中继地址,即图中的192.0.2.15:50000,另外两个对等端若要通过TURN协议和Client进行通信,
可以直接往中继地址收发数据即可,TURN服务器会把发往指定中继地址的数据转发到对应的Client,这里是其反射地址。

Server上的每一个allocation都唯一对应一个client,并且只有一个中继地址,因此当数据包到达某个中继地址时,服务器总是知道应该将其转发到什么地方。
但值得一提的是,一个Client可能在同一时间在一个Server上会有多个allocation,这和上述规则是并不矛盾的。

ICE包括了NAT、STUN、TURN。
主要工作:
第一步:找出端与端的所有路径:网卡的路径,NAT穿越后的公网ip、中继服务、多网卡、vpn等
第二步:相互传给对方路径,找出能通的路径

基本概念

Android WebRTC 入门教程 -- 模拟p2p本地视频传输

Android WebRTC 入门教程(一) – 使用相机
Android WebRTC 入门教程(二) – 模拟p2p本地视频传输

源码工程: https://github.com/LillteZheng/WebRTCDemo
今天要实现的效果:

上一章中,我们学习了 PeerConnectionFactory ,VideoCapturer 等知识实现了相机的预览。

这一章,学习一个比较重要的知识,Peerconnection 点对点传输,把本地摄像头的数据,采集之后,通过 Peerconnection 建立连接,把摄像头数据,传递给另一个 surface 显示,

效果如下:
动态太大,看视频连接 https://live.csdn.net/v/242968

device-2022-09-30-142040

一. PeerConnection

PeerConnection也就是Peer-to-Peer connection(P2P) ,顾名思义,这个类代表点对点之间的连接,以及音视频数据的传输。

PeerConnection 会通过 Sdp(SessionDescription) 和 ICE(IceCandidate) 进行媒体协商,实际就是你设备支持哪些音视频编解码格式,如果两者都支持,那么就算协商成功了。
此时通道就已经建立成功,可以从 Peerconnection 中的 MediaStream 去拿数据流了。

这里先熟悉几个只是点:

1.1 SDP (SessionDescription)

会话描述符,像 RTSP 这类协议,都需要进行媒体协商,际就是你的设备所支持的音视频编解码器、使用的传输协议、SSRC等信息…通过信令服务器透传给对方。如果双方都支持,那么就算协商成功了。

  • Offer: 呼叫方发送端的 SDP 消息叫做 offer
  • Answer:被呼叫方发送端的 SDP 消息叫做 Answer

1.2 ICE(IceCandidate)

进行媒体协商后,通道此时还未建立,就需要交换 IceCandidate,其实是一个整合STUN和TURN的框架, 给它提供STUN和TURN服务器地址, 它会自动选择优先级高的进行NAT穿透,选择一条稳定的通道。

二. 实现

首先,先创建两个 SurfaceViewRender ,一个用来摄像头渲染,一个用来接收:

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="trans"
        android:text="开始本地传输"
        />
    <org.webrtc.SurfaceViewRenderer
        android:id="@+id/localRender"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <org.webrtc.SurfaceViewRenderer
        android:id="@+id/remoteRender"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="5dp"
        android:layout_weight="1"/>


</androidx.appcompat.widget.LinearLayoutCompat>

而 remoteRender 的配置,因为我们需要等待数据渲染,所以只需要初始化等待即可:

    /**
     * 配置接收端的渲染surface
     */
    fun configRemoteRender(eglBaseContext:EglBase.Context)
        remoteRender.holder.addCallback(object:SurfaceHolder.Callback
            override fun surfaceCreated(holder: SurfaceHolder) 
                //等待接收数据,这里只要初始化即可
                remoteRender.init(eglBaseContext,null)
                remoteRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
                remoteRender.setEnableHardwareScaler(false)
            

            override fun surfaceChanged(
                holder: SurfaceHolder,
                format: Int,
                width: Int,
                height: Int
            ) 
            

            override fun surfaceDestroyed(holder: SurfaceHolder) 
            

        )
    

为了方便解释,这里的发送端使用 local,接收端使用 remote,他们的流程图如下:

2.1 添加 MediaStream

获取相机数据,上一章中,我们已经实践过了,代码不变,还未看过的可以参考上一章。
Android WebRTC 入门教程(一) – 使用相机

代码如下:

createCameraCapture()?.let  camera->
                     ....
                    // 添加渲染接收端器到轨道中,画面开始呈现
                    videoTrack.addSink(localRender)
					// 创建 mediastream
                    localMediaStream = peerConnectionFactory.createLocalMediaStream("MediaStream")
                    // 将视频轨添加到 mediastram 中,等待连接时传输 
                    localMediaStream.addTrack(videoTrack)
                

2.2 创建 Peerconnection

要进行点对点传输,就需要创建 Peerconnection ,使用 PeerconnectionFactory 创建,如

        val iceServers: List<IceServer> = ArrayList()
        //内部会转成 RTCConfiguration
        localPeerConnection = peerConnectionFactory.createPeerConnection(iceServers,object:PeerObserver()
            override fun onAddStream(strem: MediaStream?) 
                super.onAddStream(strem)
            

            override fun onIceCandidate(p0: IceCandidate?) 
                super.onIceCandidate(p0)
             

            
        )
        remotePeerConnection = peerConnectionFactory.createPeerConnection(iceServers,object :PeerObserver()
            override fun onAddStream(strem: MediaStream?) 
                super.onAddStream(strem)
               
            

            override fun onIceCandidate(p0: IceCandidate?) 
                super.onIceCandidate(p0)
            
        )

其中 PeerObserver 继承 PeerConnection.Observer ,减少一些方法的重写。

2.2 Create offer

此时,他们还没有任何联系。
根据时序图,我们需要先创建 sdp offer,并发送给接收端。
首先先添加媒体流

  localPeerConnection?.addStream(localMediaStream)

然后创建 offer,此时就要进行 sdp 媒体交换:

这个是双方协商的过程,因此会设计到两个比较特殊的方法,setLocalDescription 和setRemoteDescription,双方在创建sdp 之后,都需要使用 setLocalDescription 把 sdp 保存到本地,然后对方使用 setRemoteDescription,把对方的 sdp 保存到远程。
具体方法如下:

        //客户端创建 offer
        localPeerConnection?.createOffer(object:SdpObserver("创建local offer")
            override fun onCreateSuccess(p0: SessionDescription?) 
                super.onCreateSuccess(p0)
                Log.d(TAG, "创建 local offer success: ")
                /**
                 * 此时调用setLocalDescription()方法将该Offer保存到本地Local域,
                 * 然后将Offer发送给对方
                 */
                localPeerConnection?.setLocalDescription(SdpObserver("local 设置本地 sdp"),p0)
                //2. 同时,接收端 setRemoteDescription 把 offer 保存到远端域
                remotePeerConnection?.setRemoteDescription(SdpObserver("把 sdp 给到 remote"),p0)

                //3. 此时remote 已经接收端了 offer ,所以创建 answer
                remotePeerConnection?.createAnswer(object:SdpObserver("创建 remote answer")
                    override fun onCreateSuccess(p0: SessionDescription?) 
                        super.onCreateSuccess(p0)
                        Log.d(TAG, " 创建 remote answer success: ")
                        /**
                         * 4. 此时调用setLocalDescription()方法将该 answer 保存到本地 remote 域,
                         * 然后将 answer 发送给对方
                         */
                        remotePeerConnection?.setLocalDescription(SdpObserver("remote 设置本地 sdp"),p0)
                        //5. local 把 answer 保存到它的 remote 端,此时 sdp 交换结束
                        localPeerConnection?.setRemoteDescription(SdpObserver("把 sdp 给到 local"),p0)
                    

                , MediaConstraints())
            
        , MediaConstraints())

我们以连接上的Client端为呼叫方先发起Offer请求,通过createOffer()创建一个Offer SDP。创建成功后,会在SdpObserver中收到onCreateSuccess回调,此时调用setLocalDescription()方法将该Offer保存到本地Local域,然后将Offer发送给对方。

被呼叫方接收到Offer后,通过setRemoteDescription()方法将Offer保存到它的Remote域,并通过createAnswer()创建Answer SDP,创建成功后同样调用setLocalDescription()方法将Answer消息保存到本地的Local域,然后回复给呼叫方。

最后,呼叫方将收到Answer消息,并通过setRemoteDescription()方法,将Answer保存到它的Remote域。至此,整个媒体协商的过程结束。

其中 SdpObserver 继承 webrtc 的 SdpObserver ,减少一些方法的重写。

三. 建立点对点连接

媒体协商结束,我们的点对点并没有真正的简历。此时 PeerConnection.Observer 会回调 onIceCandidate ,我们需要将各自的 Candidate 添加给对方。
同时,因为 local 作为视频流的一方,onAddStream 不需要理会,但 remote 作为接收方,则需要把 MediaStream 中的流拿出来,放到 SurfaceViewRender 去渲染。
代码如下:

        localPeerConnection = peerConnectionFactory.createPeerConnection(iceServers,object:PeerObserver()
            override fun onAddStream(strem: MediaStream?) 
                super.onAddStream(strem)
            

            override fun onIceCandidate(p0: IceCandidate?) 
                super.onIceCandidate(p0)
                //透传给另外一端
                remotePeerConnection?.addIceCandidate(p0)

            
        )
        remotePeerConnection = peerConnectionFactory.createPeerConnection(iceServers,object :PeerObserver()
            override fun onAddStream(strem: MediaStream?) 
                super.onAddStream(strem)
                //拿到视频流,添加 sink ,SurfaceViewRender 会直接渲染
                strem?.let 
                    runOnUiThread 
                        it.videoTracks[0].addSink(remoteRender)
                    
                
            

            override fun onIceCandidate(p0: IceCandidate?) 
                super.onIceCandidate(p0)
                //透传给另外一端
                localPeerConnection?.addIceCandidate(p0)

            
        )

基本上学完这样,我们已经可以进行不同端传输音视频了。
只是sdp 的协商,改成其他信令服务器。

参考:
https://codezjx.com/posts/webrtc-android-demo/#more

以上是关于1- WebRTC传输基本知识的主要内容,如果未能解决你的问题,请参考以下文章

一文带你了解webrtc基本原理(动手实现1v1视频通话)

一文带你了解webrtc基本原理(动手实现1v1视频通话)

WebRTC介绍

WebRTC学习之ICE深入理解

Android WebRTC 入门教程 -- 模拟p2p本地视频传输

Android WebRTC 入门教程 -- 模拟p2p本地视频传输