美文网首页WebRtc
基于SRS的WebRTC直播流的Android端实现

基于SRS的WebRTC直播流的Android端实现

作者: Demon2004 | 来源:发表于2021-05-19 16:46 被阅读0次

    基于SRS的WebRTC直播流的Android端实现

    SRS部署

    通信 -- 直播 SRS -- SRS 部署与直播效果测试

    Android端代码实现

    本文主要是参照Android接入SRS WebRtc直播流实现,在此感谢原文作者。

    1. SRS WebRTC通信流程

      createOffer->setLocalDescription->接收answer->setRemoteDescription

      具体原理参看 WebRTC源码研究(29)媒体能力协商过程

    2. Android端代码实现

      • 引入WebRTC库

        implementation 'org.webrtc:google-webrtc:1.0.32006'
        
      • 基本布局实现,在布局文件中添加控件,SurfaceViewRenderer继承自SurfaceView,这里就是用来显示直播流的

        <org.webrtc.SurfaceViewRenderer
              android:id="@+id/surface_render"
              android:layout_width="match_parent"
              android:layout_height="match_parent" />
        
      • 初始化操作,设置WebRTC基本的参数

        private fun initRTC() {
            val eglBaseContext = EglBase.create().eglBaseContext;
            PeerConnectionFactory.initialize(
              PeerConnectionFactory.InitializationOptions
                .builder(requireContext().applicationContext).createInitializationOptions()
            )
            val options = PeerConnectionFactory.Options()
            val encoderFactory = DefaultVideoEncoderFactory(eglBaseContext, true, true)
            val decoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
            val peerConnectionFactory = PeerConnectionFactory.builder().setOptions(options).setVideoEncoderFactory(encoderFactory)
              .setVideoDecoderFactory(decoderFactory).createPeerConnectionFactory()
            binding?.surfaceRender?.init(eglBaseContext, null)
            val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
            // 这里不能用PLAN_B 会报错
            rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
            peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer {
              override fun onSignalingChange(p0: SignalingState?) {
                Logger.d(TAG, "onSignalingChange")
              }
        
              override fun onIceConnectionChange(p0: IceConnectionState?) {
                Logger.d(TAG, "onIceConnectionChange")
              }
        
              override fun onIceConnectionReceivingChange(p0: Boolean) {
                Logger.d(TAG, "onIceConnectionReceivingChange")
              }
        
              override fun onIceGatheringChange(p0: IceGatheringState?) {
                Logger.d(TAG, "onIceGatheringChange")
              }
        
              override fun onIceCandidate(p0: IceCandidate?) {
                Logger.d(TAG, "onIceCandidate")
              }
        
              override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
                Logger.d(TAG, "onIceCandidatesRemoved")
              }
        
              override fun onAddStream(p0: MediaStream?) {
                Logger.d(TAG, "onAddStream")
                lifecycleScope.launch(Dispatchers.Main) {
                  binding?.loadingText?.visibility = View.GONE
                }
                // 当连接成功建立之后,会在这个回掉里返回数据流
                p0?.videoTracks?.get(0)?.addSink(binding?.surfaceRender!!)
              }
        
              override fun onRemoveStream(p0: MediaStream?) {
                Logger.d(TAG, "onRemoveStream")
              }
        
              override fun onDataChannel(p0: DataChannel?) {
                Logger.d(TAG, "onDataChannel")
              }
        
              override fun onRenegotiationNeeded() {
                Logger.d(TAG, "onRenegotiationNeeded")
              }
        
              override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
                Logger.d(TAG, "onAddTrack")
              }
        
            })
            peerConnection?.addTransceiver(
              MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO,
              RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)
            )
            peerConnection?.addTransceiver(
              MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO,
              RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)
            )
          }
        
      • 声明SdpObserver,在peerConnection的createOffer和setRemoteDescription中会需要传递这个类型的参数,看回调的方法名大概知道这个observer主要用来观察创建和设置成功失败与否。

        private val sdbObserver = object : SdpObserver {
            override fun onCreateSuccess(p0: SessionDescription?) {
              // 判断当前的创建成功类型,如果是offer类型的则进行下一步处理
              p0?.takeIf { it.type == SessionDescription.Type.OFFER }.let {
                offerSdp = it?.description?:""
                peerConnection?.setLocalDescription(this, it)
                // 创建offer成功后 请求SRS服务器接口 
                // POST 请求 得到结果后调用setRemoteDescription传入返回的sdp
                // mWebRtcUrl 是一个webrtc开头的地址 类似 webrtc://10.1.1.1/live/1
                it?.description?.let { sdp -> mViewModel.requestPlay(mWebRtcUrl, sdp) }
              }
            }
        
            override fun onSetSuccess() {
              Logger.d(TAG, "onSetSuccess ")
            }
        
            override fun onCreateFailure(p0: String?) {
              Logger.d(TAG, "onCreateFailure $p0 ")
              lifecycleScope.launch(Dispatchers.Main) {
                shortToast("开启视频失败")
              }
            }
        
            override fun onSetFailure(p0: String?) {
              Logger.d(TAG, "onSetFailure $p0 ")
              lifecycleScope.launch(Dispatchers.Main) {
                shortToast("开启视频失败")
              }
            }
          }
        
      • 请求SRS服务器接口
        接口地址 http://[IP地址]:[端口号]/rtc/v1/play/ 端口号默认是1985,具体与服务器协商

        请求参数

        参数名 类型 备注
        streamurl String webrtc开头的视频流播放地址,就是上一步备注当中的mWebRtcUrl
        sdp String 创建offer成功后的sdp,代码中通过SessionDescription.description获取

        注意,该请求参数不能用Json格式传递

        我这里用的是Retrofit,具体的各位可根据自己的网络请求库进行处理

        @POST()
        suspend fun requestPlay(@Url url: String = "http://[ip]:[port]/rtc/v1/play/", @Body requestBody: SRSRequestBody): SrsResponse
        

        返回值

        {
            "code": 0,
            "server": "vid-q502b4b",
            "sdp": "..........",
            "sessionid": "75ol7881:WE/k"
        }
        

        下一步需要设置remoteDescription

      • setRemoteDescription

        private fun setRemoteDescription(sdp: String) {
           //createOffer生成的sdp与request请求返回的sdp 'm='顺序要保证一致,如果offer返回的sdp 先是m=video 然后是m=audio
           //那么setRemoteDescription的时候的sdp也要保证一样的顺序,但是目前发现通过srs请求回来的sdp可能不符合这个要求,所以
           //用这个方法进行判断并且重新排序
            reorderSdp(sdp)
            val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, reorderSdp(sdp))
            peerConnection?.setRemoteDescription(sdbObserver, remoteSdp)
          }
        

        这里需要注意的是注释的这个地方,这里对这个顺序有严格的要求,如果你出现了Failed to set remote answer sdp: The order of m-lines in answer doesn't match order in offer. Reject这个错误,记得检查一下createOff之后获得的sdp与请求返回的sdp的顺序是否一致

        image-20210519162709896

        如上图,左边是createOffer之后返回的sdp,右边是请求之后返回的sdp,在第一个m=的地方,左边的是video,右边的是audio,这个就是顺序不一致,需要自己本地处理一下,我自己的处理就是截取字符串重新拼接,代码写的有点丑陋,仅做参考,谁要是有更好的办法可以告知一下,谢谢。�

        private fun reorderSdp(sdp: String):String {
            if(offerSdp.isEmpty()) {
              return  sdp
            }
            val offerFirstM = offerSdp.substring(offerSdp.indexOf("m="), offerSdp.lastIndexOf("m="))
            val firstM = sdp.substring(sdp.indexOf("m="), sdp.lastIndexOf("m="))
            if(offerFirstM.indexOf("m=video") == firstM.indexOf("m=video")) {
              return sdp
            }
            val start = sdp.substring(0, sdp.indexOf("m="))
            val lastM = sdp.substring(sdp.lastIndexOf("m="), sdp.length)
            Logger.d(TAG, "reOrderSdp ${start + lastM + firstM}")
            return start + lastM + firstM
        
          }
        

        如果这些地方都处理完毕,那么在最开始初始化rtc的地方createPeerConnection方法的回调当中就可以进行一些其余的数据处理,最后一行用来展示视频

        override fun onAddStream(p0: MediaStream?) {
                Logger.d(TAG, "onAddStream")
                lifecycleScope.launch(Dispatchers.Main) {
                  binding?.loadingText?.visibility = View.GONE
                }
                // 当连接成功建立之后,会在这个回掉里返回数据流
                p0?.videoTracks?.get(0)?.addSink(binding?.surfaceRender!!)
              }
        

    相关文章

      网友评论

        本文标题:基于SRS的WebRTC直播流的Android端实现

        本文链接:https://www.haomeiwen.com/subject/pkqmhftx.html