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

基于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,这里就是用来显示直播流的

      
      
    • 初始化操作,设置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?) {
              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?) {
              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端实现)