基于SRS服务器实现Android-Web端视频通话(2):Android端从SRS服务器拉取WebRTC流

基于SRS服务器实现Android-Web端视频通话(1):SRS服务器启用HTTPS
基于SRS服务器实现Android-Web端视频通话(2):Android端从SRS服务器拉取WebRTC流
基于SRS服务器实现Android-Web端视频通话(3):Android端向SRS服务器推送WebRTC流

实现效果

实现效果

引库

implementation 'org.webrtc:google-webrtc:1.0.32006'

其他版本,详见

拉流流程

createPeerConnectionFactory -> createPeerConnection -> createOffer -> setLocalDescription(OFFER) -> get remote sdp(network requset) -> setRemoteDescription(ANSWER)

代码实现

初始化

//加载并初始化 WebRTC,在创建 PeerConnectionFactory 之前必须至少调用一次
PeerConnectionFactory.initialize(
    PeerConnectionFactory.InitializationOptions
        .builder(applicationContext).createInitializationOptions()
)

private val eglBaseContext = EglBase.create().eglBaseContext

createPeerConnectionFactory

private lateinit var peerConnectionFactory: PeerConnectionFactory
...
//一些默认初始化配置即可
val options = PeerConnectionFactory.Options()
val encoderFactory = DefaultVideoEncoderFactory(eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
peerConnectionFactory = PeerConnectionFactory.builder()
    setOptions(options)
    .setVideoEncoderFactory(encoderFactory)
    .setVideoDecoderFactory(decoderFactory)
    .createPeerConnectionFactory()
...

createPeerConnection

val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
/*
 <p>For users who wish to send multiple audio/video streams and need to stay interoperable with legacy WebRTC implementations, specify PLAN_B.
 <p>For users who wish to send multiple audio/video streams and/or wish to use the new RtpTransceiver API, specify UNIFIED_PLAN.
 */
//使用PeerConnection.SdpSemantics.UNIFIED_PLAN
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
val peerConnection = peerConnectionFactory.createPeerConnection(
    rtcConfig,
    object : PeerConnectionObserver() {
    	/**
    	 * Triggered when media is received on a new stream from remote peer.
    	 * 当收到远端媒体流时调用
    	 */
        override fun onAddStream(mediaStream: MediaStream?) {
            super.onAddStream(mediaStream)
            mediaStream?.let {
            	//如果有视频轨。
                if (it.videoTracks.isEmpty().not()) {
                    it.videoTracks[0].addSink(mBinding.svr)
                }
            }
        }
    })?.apply {
    //addTransceiver()调用顺序会影响到生成sdp信息中video、audio信息顺序,对应关系,下面会提到;
    //接收视频,指定仅接收即可
    addTransceiver(
        MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO,
        RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)
    )
    //接收音频,指定仅接收即可
    addTransceiver(
        MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO,
        RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)
    )
}

createOffer && setLocalDescription

peerConnection.createOffer(object : SdpAdapter("createOffer") {
    override fun onCreateSuccess(description: SessionDescription?) {
        super.onCreateSuccess(description)
        description?.let {
            if (it.type == SessionDescription.Type.OFFER) {     
            	peerConnection.setLocalDescription(SdpAdapter("setLocalDescription"), it)
            	//这个offerSdp将用于向SRS服务进行网络请求
				val offerSdp = it.description
				getRemoteSdp(offerSdp)             
            }
        }
    }
}, MediaConstraints())

get remote sdp(netword requset)

基本配置,根据自己实际情况进行调整

object Constant {
    /**
     * SRS服务器IP
     */
    const val SRS_SERVER_IP = "192.168.2.91"

    /**
     * SRS服务http请求端口,默认1985
     */
    const val SRS_SERVER_HTTP_PORT = "1985"

    /**
     * SRS服务https请求端口,默认1990
     */
    const val SRS_SERVER_HTTPS_PORT = "1990"

    const val SRS_SERVER_HTTP = "$SRS_SERVER_IP:$SRS_SERVER_HTTP_PORT"

    const val SRS_SERVER_HTTPS = "$SRS_SERVER_IP:$SRS_SERVER_HTTPS_PORT"
}

Request Body (application/json)

data class SrsRequestBean(
    /**
     * [PeerConnection.createOffer]返回的sdp
     */
    @Json(name = "sdp")
    val sdp: String?,
    /**
     * 拉取的WebRTC流地址
     */
    @Json(name = "streamurl")
    val streamUrl: String?
)

Response Body (application/json)

data class SrsResponseBean(
    /**
     * 0:成功
     */
    @Json(name = "code")
    val code: Int,
    /**
     * 用于设置[PeerConnection.setRemoteDescription]
     */
    @Json(name = "sdp") val sdp: String?,
    @Json(name = "server")
    val server: String?,
    @Json(name = "sessionid")
    val sessionId: String?
)

网络请求地址
http请求:http://ip:port/rtc/v1/play/
https请求:https://ip:port/rtc/v1/play/
Method:POST

在Android P(28)系统的设备上,禁止应用使用的是非加密的明文流量的HTTP 网络请求。

retrofit事例

interface ApiService {

    @POST("/rtc/v1/play/")
    suspend fun play(@Body body: SrsRequestBean): SrsResponseBean
}

getRemoteSdp

private fun getRemoteSdp(offerSdp: String){
	//webrtc流地址
	val webrtcUrl="webrtc://${Constant.SRS_SERVER_IP}/live/livestream"
    val srsBean = SrsRequestBean(offerSdp, webrtcUrl)
    lifecycleScope.launch {
        val result = try {
            withContext(Dispatchers.IO) {
                retrofitClient.apiService.play(srsBean)
            }
        } catch (e: Exception) {
            println("网络请求出错:${e.printStackTrace()}")
            toastError("网络请求出错:${e.printStackTrace()}")
            null
        }

        result?.let { bean ->
            if (bean.code == 0) {
                println("网络请求成功,code:${bean.code}")
				setRemoteDescription(bean.sdp)
            } else {
                println("网络请求失败,code:${bean.code}")
            }
        }
    }
}

setRemoteDescription

private fun setRemoteDescription(answerSdp: String){
	val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, /*关键点*/answerSdp)
	//注意这一步,可能会报错:Failed to set remote answer sdp: The order of m-lines in answer doesn't match order in offer. Rejecting answer.
	peerConnection.setRemoteDescription(SdpAdapter("setRemoteDescription"), remoteSdp)
}

如果你遇到这个错误:
Failed to set remote answer sdp: The order of m-lines in answer doesn’t match order in offer. Rejecting answer.

可以看下我的另外一篇博客
具体原因就是offer中sdp的video、audio和answer中sdp的video、audio顺序不一致,详见#3179
解决方案目前大致有三种:
1、升级SRS服务器版本,截止2022-09-16,升级到v4.0.265及以上,即可解决,云服务器这个版本做了兼容处理;
2、如果无法升级SRS,可以调整上面提到的调整**addTransceiver()**调用顺序,确保顺序与SRS接口返回的sdp中video、audio顺序一致;

ps: 上面两种方案解决方式还要非常感谢SRS-杨成立指点,十分感谢!

3、使用博客中第一种原因的解决方式,我们需要手动调换下位置(当然这个方法也可以作为最后的保护手段):

/**
 * 转换AnswerSdp
 * @param offerSdp offerSdp:创建offer时生成的sdp
 * @param answerSdp answerSdp:网络请求srs服务器返回的sdp
 * @return 转换后的AnswerSdp
 */
private fun convertAnswerSdp(offerSdp: String, answerSdp: String?): String {
	if (answerSdp.isNullOrBlank()){
		return ""
    }
    val indexOfOfferVideo = offerSdp.indexOf("m=video")
    val indexOfOfferAudio = offerSdp.indexOf("m=audio")
    if (indexOfOfferVideo == -1 || indexOfOfferAudio == -1) {
        return answerSdp
    }
    val indexOfAnswerVideo = answerSdp.indexOf("m=video")
    val indexOfAnswerAudio = answerSdp.indexOf("m=audio")
    if (indexOfAnswerVideo == -1 || indexOfAnswerAudio == -1) {
        return answerSdp
    }

    val isFirstOfferVideo = indexOfOfferVideo < indexOfOfferAudio
    val isFirstAnswerVideo = indexOfAnswerVideo < indexOfAnswerAudio
    return if (isFirstOfferVideo == isFirstAnswerVideo) {
        //顺序一致
        answerSdp
    } else {
        //需要调换顺序
        buildString {
            append(answerSdp.substring(0, indexOfAnswerVideo.coerceAtMost(indexOfAnswerAudio)))
            append(
                answerSdp.substring(
                    indexOfAnswerVideo.coerceAtLeast(indexOfOfferVideo),
                    answerSdp.length
                )
            )
            append(
                answerSdp.substring(
                    indexOfAnswerVideo.coerceAtMost(indexOfAnswerAudio),
                    indexOfAnswerVideo.coerceAtLeast(indexOfOfferVideo)
                )
            )
        }
    }
}

修改方法:

private fun setRemoteDescription(offerSdp: String, answerSdp: String){
	val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, /*关键点*/convertAnswerSdp(offerSdp, answerSdp))
	peerConnection.setRemoteDescription(SdpAdapter("setRemoteDescription"), remoteSdp)
}

关闭

释放资源,避免内存泄漏

mBinding.svr.release()
peerConnection?.dispose()
peerConnectionFactory.dispose()

至此,拉流播放流程结束。如有错误欢迎指正。

Github传送门

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冬季穿短裤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值