Android-based Video Call on the SRS Server (1) : ENABLE HTTPS on the SRS server (2) : The Android terminal pulls WebRTC streams from the SRS server To implement Android-based Web video calls based on the SRS server (3) : The Android terminal pushes WebRTC streams to the SRS server

Implementation effect

Lead library

implementation 'org. Webrtc: Google - webrtc: 1.0.32006'
Copy the code

For other versions, see

Pull flow process

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

Code implementation

Initialize the

// Load and initialize WebRTC, which must be called at least once before PeerConnectionFactory is created
PeerConnectionFactory.initialize(
    PeerConnectionFactory.InitializationOptions
        .builder(applicationContext).createInitializationOptions()
)

private val eglBaseContext = EglBase.create().eglBaseContext
Copy the code

createPeerConnectionFactory

private lateinit var peerConnectionFactory: PeerConnectionFactory
...
// Some default initial configurations will do
val options = PeerConnectionFactory.Options()
val encoderFactory = DefaultVideoEncoderFactory(eglBaseContext, true.true)
val decoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
peerConnectionFactory = PeerConnectionFactory.builder()
    setOptions(options)
    .setVideoEncoderFactory(encoderFactory)
    .setVideoDecoderFactory(decoderFactory)
    .createPeerConnectionFactory()
...
Copy the code

createPeerConnection

val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
/* 

For users who wish to send multiple audio/video streams and need to stay interoperable with legacy WebRTC implementations, specify PLAN_B.

For users who wish to send multiple audio/video streams and/or wish to use the new RtpTransceiver API, specify UNIFIED_PLAN. */

/ / use the 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. */ Triggered when media is received on a new stream from remote peer override fun onAddStream(mediaStream: MediaStream?). { super.onAddStream(mediaStream) mediaStream? .let {// If there is a video track. if (it.videoTracks.isEmpty().not()) { it.videoTracks[0].addSink(mBinding.svr) } } } })? .apply {// Accept video only addTransceiver( MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY) ) // Receive audio only addTransceiver( MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY) ) } Copy the code

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)
            	// This office DP will be used to make network requests to the SRS service
				val offerSdp = it.description
				getRemoteSdp(offerSdp)             
            }
        }
    }
}, MediaConstraints())
Copy the code

get remote sdp(netword requset)

The basic configuration can be adjusted according to the actual situation

object Constant {
    /** * SRS server IP address */
    const val SRS_SERVER_IP = "192.168.2.91"

    /** * SRS service HTTP request port, default 1985 */
    const val SRS_SERVER_HTTP_PORT = "1985"

    /** * SRS service HTTPS request port, default 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"
}
Copy the code

Request Body (application/json)

data class SrsRequestBean(
    / * * * [PeerConnection createOffer] returns the SDP * /
    @Json(name = "sdp")
    valsdp: String? ./** * Pull the WebRTC stream address */
    @Json(name = "streamurl")
    val streamUrl: String?
)
Copy the code

Response Body (application/json)

data class SrsResponseBean(
    /** * 0: successful */
    @Json(name = "code")
    val code: Int./ * * * is used to set [PeerConnection setRemoteDescription] * /
    @Json(name = "sdp") valsdp: String? .@Json(name = "server")
    valserver: String? .@Json(name = "sessionid")
    val sessionId: String?
)
Copy the code

Network address HTTP request: http://ip:port/rtc/v1/play/ HTTPS requests: https://ip:port/rtc/v1/play/ Method: POST

On Android P(28) devices, forbid applications to use HTTP network requests with unencrypted plaintext traffic.

Retrofit example

interface ApiService {

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

getRemoteSdp

private fun getRemoteSdp(offerSdp: String){
    / / address webrtc flow
    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("Network request error:${e.printStackTrace()}")
            toastError("Network request error:${e.printStackTrace()}")
            null} result? .let { bean ->if (bean.code == 0) {
                println("Network request successful, code:${bean.code}")
				setRemoteDescription(bean.sdp)
            } else {
                println("Network request failed, code:${bean.code}")}}}}Copy the code

setRemoteDescription

private fun setRemoteDescription(answerSdp: String){
	val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, /* Key points */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)
}
Copy the code

Failed to set remote answer SDP: The order of m-lines in answer doesn’t match order in offer. Rejecting answer.

Take a look at my other blog post.

Check the offer SDP created on Android and the answer SDP returned from SRS:

offer sdp answer sdp

Obviously, the problem is the first one in the blog category. We need to manually switch positions.

/** * convert AnswerSdp *@paramOfferSdp offerSdp: The SDP generated when an offer is created@paramAnswerSdp answerSdp: The NETWORK requests the SDP returned by the SRS server@returnAfter conversion 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) {
        // The sequence is consistent
        answerSdp
    } else {
        // The order needs to be reversed
        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) ) ) } } }Copy the code

Modification method:

private fun setRemoteDescription(offerSdp: String, answerSdp: String){
	val remoteSdp = SessionDescription(SessionDescription.Type.ANSWER, /* Key points */convertAnswerSdp(offerSdp, answerSdp))
	peerConnection.setRemoteDescription(SdpAdapter("setRemoteDescription"), remoteSdp)
}
Copy the code

Shut down

Free up resources to avoid memory leaks

mBinding.svr.release() peerConnection? .dispose() peerConnectionFactory.dispose()Copy the code

At this point, the pull-stream playback process ends. You are welcome to correct any mistakes.

Making portal