基于SRS的WebRTC直播流的Android端實(shí)現(xiàn)

基于SRS的WebRTC直播流的Android端實(shí)現(xiàn)

SRS部署

通信 -- 直播 SRS -- SRS 部署與直播效果測(cè)試

Android端代碼實(shí)現(xiàn)

本文主要是參照Android接入SRS WebRtc直播流實(shí)現(xiàn),在此感謝原文作者。

  1. SRS WebRTC通信流程

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

    具體原理參看 WebRTC源碼研究(29)媒體能力協(xié)商過(guò)程

  2. Android端代碼實(shí)現(xiàn)

    • 引入WebRTC庫(kù)

      implementation 'org.webrtc:google-webrtc:1.0.32006'
      
    • 基本布局實(shí)現(xiàn),在布局文件中添加控件,SurfaceViewRenderer繼承自SurfaceView,這里就是用來(lái)顯示直播流的

      <org.webrtc.SurfaceViewRenderer
            android:id="@+id/surface_render"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
      
    • 初始化操作,設(shè)置WebRTC基本的參數(shù)

      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 會(huì)報(bào)錯(cuò)
          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
              }
              // 當(dāng)連接成功建立之后,會(huì)在這個(gè)回掉里返回?cái)?shù)據(jù)流
              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中會(huì)需要傳遞這個(gè)類型的參數(shù),看回調(diào)的方法名大概知道這個(gè)observer主要用來(lái)觀察創(chuàng)建和設(shè)置成功失敗與否。

      private val sdbObserver = object : SdpObserver {
          override fun onCreateSuccess(p0: SessionDescription?) {
            // 判斷當(dāng)前的創(chuàng)建成功類型,如果是offer類型的則進(jìn)行下一步處理
            p0?.takeIf { it.type == SessionDescription.Type.OFFER }.let {
              offerSdp = it?.description?:""
              peerConnection?.setLocalDescription(this, it)
              // 創(chuàng)建offer成功后 請(qǐng)求SRS服務(wù)器接口 
              // POST 請(qǐng)求 得到結(jié)果后調(diào)用setRemoteDescription傳入返回的sdp
              // mWebRtcUrl 是一個(gè)webrtc開(kāi)頭的地址 類似 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("開(kāi)啟視頻失敗")
            }
          }
      
          override fun onSetFailure(p0: String?) {
            Logger.d(TAG, "onSetFailure $p0 ")
            lifecycleScope.launch(Dispatchers.Main) {
              shortToast("開(kāi)啟視頻失敗")
            }
          }
        }
      
    • 請(qǐng)求SRS服務(wù)器接口
      接口地址 http://[IP地址]:[端口號(hào)]/rtc/v1/play/ 端口號(hào)默認(rèn)是1985,具體與服務(wù)器協(xié)商

      請(qǐng)求參數(shù)

      參數(shù)名 類型 備注
      streamurl String webrtc開(kāi)頭的視頻流播放地址,就是上一步備注當(dāng)中的mWebRtcUrl
      sdp String 創(chuàng)建offer成功后的sdp,代碼中通過(guò)SessionDescription.description獲取

      注意,該請(qǐng)求參數(shù)不能用Json格式傳遞

      我這里用的是Retrofit,具體的各位可根據(jù)自己的網(wǎng)絡(luò)請(qǐng)求庫(kù)進(jìn)行處理

      @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"
      }
      

      下一步需要設(shè)置remoteDescription

    • setRemoteDescription

      private fun setRemoteDescription(sdp: String) {
         //createOffer生成的sdp與request請(qǐng)求返回的sdp 'm='順序要保證一致,如果offer返回的sdp 先是m=video 然后是m=audio
         //那么setRemoteDescription的時(shí)候的sdp也要保證一樣的順序,但是目前發(fā)現(xiàn)通過(guò)srs請(qǐng)求回來(lái)的sdp可能不符合這個(gè)要求,所以
         //用這個(gè)方法進(jìn)行判斷并且重新排序
          reorderSdp(sdp)
          val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, reorderSdp(sdp))
          peerConnection?.setRemoteDescription(sdbObserver, remoteSdp)
        }
      

      這里需要注意的是注釋的這個(gè)地方,這里對(duì)這個(gè)順序有嚴(yán)格的要求,如果你出現(xiàn)了Failed to set remote answer sdp: The order of m-lines in answer doesn't match order in offer. Reject這個(gè)錯(cuò)誤,記得檢查一下createOff之后獲得的sdp與請(qǐng)求返回的sdp的順序是否一致

      image-20210519162709896

      如上圖,左邊是createOffer之后返回的sdp,右邊是請(qǐng)求之后返回的sdp,在第一個(gè)m=的地方,左邊的是video,右邊的是audio,這個(gè)就是順序不一致,需要自己本地處理一下,我自己的處理就是截取字符串重新拼接,代碼寫(xiě)的有點(diǎn)丑陋,僅做參考,誰(shuí)要是有更好的辦法可以告知一下,謝謝。?

      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
      
        }
      

      如果這些地方都處理完畢,那么在最開(kāi)始初始化rtc的地方createPeerConnection方法的回調(diào)當(dāng)中就可以進(jìn)行一些其余的數(shù)據(jù)處理,最后一行用來(lái)展示視頻

      override fun onAddStream(p0: MediaStream?) {
              Logger.d(TAG, "onAddStream")
              lifecycleScope.launch(Dispatchers.Main) {
                binding?.loadingText?.visibility = View.GONE
              }
              // 當(dāng)連接成功建立之后,會(huì)在這個(gè)回掉里返回?cái)?shù)據(jù)流
              p0?.videoTracks?.get(0)?.addSink(binding?.surfaceRender!!)
            }
      
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容