基于SRS的WebRTC直播流的Android端實(shí)現(xiàn)
SRS部署
通信 -- 直播 SRS -- SRS 部署與直播效果測(cè)試
Android端代碼實(shí)現(xiàn)
本文主要是參照Android接入SRS WebRtc直播流實(shí)現(xiàn),在此感謝原文作者。
-
SRS WebRTC通信流程
createOffer->setLocalDescription->接收answer->setRemoteDescription
-
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!!) }
-
