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

Posted 夏至的稻穗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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>

采集摄像头数据的,我们使用 localRender ,它的具体实现,可参考上一章 :
Android WebRTC 入门教程(一) – 使用相机

接收端使用 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

以上是关于Android WebRTC 入门教程 -- 模拟p2p本地视频传输的主要内容,如果未能解决你的问题,请参考以下文章

Webrtc入门——基于阿里云ubuntu 最新webrtc Android平台编译详细说明

Android WebRTC 入门教程 -- 使用相机

Android WebRTC完整入门教程02: 本地回环

Android WebRTC 入门教程 -- 使用相机

Android技术分享| Android WebRTC 开启H264软件编解码教程

Flutter webrtc音频无法在android上运行