Android 中使用 WebRtc 將影片直播到 SRS,這方面的資訊非常少,也有很多坑,所以撰寫此篇詳細說明。
公用套件
接下來下載公用套件放在專案中。
請由如下網址下載 : webrtc_http.zip。
G class
G 檔是儲存通用的資訊,請新增 G.kt,完整內容如下
object G { const val SRS_SERVER_IP = "mahalbot.ddns.net" const val SRS_SERVER_HTTP_PORT = "1985" const val SRS_SERVER_HTTPS_PORT = "8080" const val SRS_SERVER_HTTP = "${SRS_SERVER_IP}:$SRS_SERVER_HTTP_PORT" const val SRS_SERVER_HTTPS = "${SRS_SERVER_IP}:$SRS_SERVER_HTTPS_PORT" }
onCreate
onCreate 需新增 globalInit,否則會連線失敗
import android.content.Context
import android.media.MediaCodecInfo
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.shencoder.mvvmkit.ext.globalInit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.ddns.mahaljsp.cameraw.databinding.ActivityMainBinding
import net.ddns.mahaljsp.cameraw.http.RetrofitClient
import net.ddns.mahaljsp.cameraw.http.SrsRequestBean
import org.koin.android.ext.android.inject
import org.koin.android.java.KoinAndroidApplication
import org.koin.core.logger.Level
import org.koin.dsl.module
import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator
import org.webrtc.Camera2Enumerator
import org.webrtc.CameraEnumerator
import org.webrtc.CameraVideoCapturer
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.EglBase
import org.webrtc.MediaConstraints
import org.webrtc.PeerConnection
import org.webrtc.PeerConnectionFactory
import org.webrtc.RendererCommon
import org.webrtc.RtpTransceiver
import org.webrtc.SessionDescription
import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoEncoderSupportedCallback
import org.webrtc.VideoTrack
import org.webrtc.createCustomVideoEncoderFactory
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import net.ddns.mahaljsp.cameraw.http.PeerConnectionObserver
import net.ddns.mahaljsp.cameraw.http.SdpAdapter
class MainActivity : AppCompatActivity() {
lateinit var permission:MainPermission
lateinit var ui:ActivityMainBinding
lateinit var rtcConfig : PeerConnection.RTCConfiguration
var surfaceTextureHelper: SurfaceTextureHelper? = null
var videoTrack: VideoTrack? = null
var audioTrack: AudioTrack? = null
val eglBaseContext = EglBase.create().eglBaseContext
lateinit var peerConnectionFactory: PeerConnectionFactory
var isStreaming=false
var peerConnection:PeerConnection?=null
lateinit var context:Context
val retrofitClient by inject<RetrofitClient>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//enableEdgeToEdge()
ui= ActivityMainBinding.inflate(layoutInflater)
setContentView(ui.root)
//無此段會連線失敗
globalInit(false,"TravelX", KoinAndroidApplication
.create(this, Level.ERROR)
.modules(mutableListOf(module {single{ RetrofitClient() }}).apply {})
)
context=this
permission= MainPermission(this)
}
}
startStream
撰寫 startStream() 方法如下
fun startStream(url:String){ peerConnection = peerConnectionFactory.createPeerConnection( rtcConfig, PeerConnectionObserver() ) peerConnection?.addTransceiver( videoTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY) ) peerConnection?.addTransceiver( audioTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY) ) peerConnection?.let { connection -> connection.createOffer(object : SdpAdapter("createOffer") { override fun onCreateSuccess(description: SessionDescription?) { super.onCreateSuccess(description) description?.let { if (it.type == SessionDescription.Type.OFFER) { val offerSdp = it.description connection.setLocalDescription(SdpAdapter("setLocalDescription"), it) val srsBean = SrsRequestBean( it.description, url ) lifecycleScope.launch{ val result = try { withContext(Dispatchers.IO) { retrofitClient.apiService.publish(srsBean) } } catch (e: Exception) { Toast.makeText(context,"網路連線失敗", Toast.LENGTH_SHORT).show() null } result?.let { bean -> if (bean.code == 0) { Toast.makeText(context,"網路連線成功", Toast.LENGTH_SHORT).show() val remoteSdp = SessionDescription( SessionDescription.Type.ANSWER, convertAnswerSdp(offerSdp, bean.sdp) ) connection.setRemoteDescription( SdpAdapter("setRemoteDescription"), remoteSdp ) } else { Toast.makeText(context,"網路連線失敗", Toast.LENGTH_SHORT).show() } } } } } } }, MediaConstraints() ) } isStreaming=true }
stopStream
stopStream 方法如下
fun stopStream(){ if (peerConnection!=null) { peerConnection?.dispose() peerConnection = null } isStreaming=false }
convertAnswerSdp
convertAnswerSdp 方法如下
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) ) ) } } }
cbtnVideo_click
btnVideo_click 方法如下
fun btnVideo_click(view:View){ fun btnVideo_click(view: View){ if(!isStreaming) { startStream("rtmp://mahalbot.ddns.net/live/video4") ui.btnVideo.setBackgroundResource(R.drawable.ic_video_stop) } else { stopStream() ui.btnVideo.setBackgroundResource(R.drawable.ic_video_start) } }
SRS 錄影中斷
WebRtc 有分辨率自適應調整的功能,當網路訊號不好時,自動將分辨率調小降低影像品質,訊號良好時自動將分辨率調高提升影像品質。這種機制雖說很先進,但一遇到 SRS 就麻煩了。
SRS 伺服器如果啟動 dvr 錄影時,會因為影像的寬高解析度變更而中斷錄影,日誌會出現如下錯誤
dvr: ignore video error code=3085(Mp4AvccChange)(MP4 does not support video AVCC change)
: consume video : write video : encode video : write sample : doesn't support avcc change
而在 SRS 官網上也有說明
WebRTC recording is a challenge, this thing often changes sps/pps.
Android WebRtc 在啟動直播後,會開始由最低解析度一直往上調整,測試那一種方式最適合目前的網路。這也就造成了 SRS 只錄了約 10 秒左右的低品質解析度後就中斷了。為了解決這個惱人的問題,就把解析度固定成 1920 * 1080 即可。
請由下方藍色代碼著手,找到 videoSender 後,設定裏面的參數。
peerConnection = peerConnectionFactory.createPeerConnection( rtcConfig, PeerConnectionObserver() ) peerConnection?.addTransceiver( videoTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY) ) peerConnection?.addTransceiver( audioTrack, RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY) ) val videoSender = peerConnection?.senders?.find { it.track()?.kind() == "video" } videoSender?.let { val parameters = it.parameters // 禁用自動調整,設置固定的比特率和幀率 parameters.encodings[0].maxBitrateBps = 4_500_000 // 設定固定的比特率 (例如 1.5 Mbps) parameters.encodings[0].minBitrateBps = 4_500_000 // 最小比特率也設為 1.5 Mbps parameters.encodings[0].maxFramerate = 25 // 固定幀率為 30 fps //scaleResolutionDownBy 範圍為 1.0~7.0, 3.0 會從640*360 跳 1920*1080, // 這樣 srs 就不會偵測到 avc change 而停止錄影 parameters.encodings[0].scaleResolutionDownBy = 3.0 it.parameters = parameters }
上面最重要的是 scaleResolutionDownBy,其值設為 1.0 時,會測試 640*480、1080*720、1920*1080。
若調整為 3.0 時,會快速的由 640*480 直接跳 1920*1080,這時 SRS 還沒收到 640*480 的影片,所以就可以避開錄影中斷的問題。