基于SRS的WebRTC直播流的Android端实现
SRS部署
通信 -- 直播 SRS -- SRS 部署与直播效果测试
Android端代码实现
本文主要是参照Android接入SRS WebRtc直播流实现,在此感谢原文作者。
-
SRS WebRTC通信流程
createOffer->setLocalDescription->接收answer->setRemoteDescription
具体原理参看 WebRTC源码研究(29)媒体能力协商过程
-
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的顺序是否一致
如上图,左边是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!!) }
-