Если вы решили сделать собственное приложение для стриминга на Android, при разработке нужно учесть множество разных нюансов. Например, зрители могут свернуть вашу трансляцию в процессе просмотра, а через какое-то время вернуться обратно. Как должно при этом работать приложение? Должна ли трансляция приостановиться или идти фоном?
Какое именно поведение реализовать в подобной ситуации — решать вам. Но очень важно предусмотреть подобные моменты технически, чтобы при сворачивании всё работало так, как вы задумали.
Этот материал — продолжение моей предыдущей статьи про создание мобильного приложения для стриминга на Android. В ней я рассказывал о базовых моментах разработки. А сейчас поговорим о нюансах. Расскажу, как технически реализовать приостановку трансляции и фоновый стриминг на Android с помощью опенсорс-библиотеки rtmp-rtsp-stream-client-java.
Фоновый стриминг
Сначала разберём кейс, когда приложение переходит в фон и обратно на передний план. Если заглянуть чуть глубже в исходный код rtmp-rtsp-stream-client-java, станет понятно, что стриминг сам по себе проходит в отдельном потоке:
package com.pedro.rtmp.rtmpclass RtmpClient(private val connectCheckerRtmp: ConnectCheckerRtmp) { //... @JvmOverloads fun connect(url: String?, isRetry: Boolean = false) { //... if (!isStreaming || isRetry) { //... isStreaming = true thread = Executors.newSingleThreadExecutor() thread?.execute post@{ try { if (!establishConnection()) { connectCheckerRtmp.onConnectionFailedRtmp("Handshake failed") return@post } val writer = this.writer ?: throw IOException("Invalid writer, Connection failed") commandsManager.sendChunkSize(writer) commandsManager.sendConnect("", writer) //read packets until you did success connection to server and you are ready to send packets while (!Thread.interrupted() && !publishPermitted) { //Handle all command received and send response for it. handleMessages() } //read packet because maybe server want send you something while streaming handleServerPackets() } catch (e: Exception) { Log.e(TAG, "connection error", e) connectCheckerRtmp.onConnectionFailedRtmp("Error configure stream, ${e.message}") return@post } } } } //...}
Это очень упрощает задачу. Получается, нам не нужно пытаться самим вынести этот процесс в отдельный поток.
Но кроме этого нужно учитывать жизненный цикл компонента, в котором у нас инициализируется стриминг, чтобы быть уверенными, что с нашим объектом для стриминга и с самим вещанием ничего не произойдет. Поэтому я решил инициализировать стриминг во ViewModel. Он остается живым на протяжении всех жизненных процессов компонента, к которому привязан (Activity, Fragment).
Замечу, что это лишь один из способов, и можно использовать и другие: например, Foreground Service.
В жизненном цикле ViewModel ничего не изменится, даже если произойдет смена конфигурации, ориентации, переход в фон или что-нибудь ещё в этом роде. Но одна проблема всё-таки есть. Для стриминга нужно создать объект RtmpCamera2(). Он зависит от объекта OpenGlView, а это элемент UI, и значит, он уничтожится при переходе приложения в фон. И дальнейшее вещание станет невозможно.
К счастью, в библиотеке предусмотрена возможность заменять на лету View объекта RtmpCamera2. Мы можем заменить её любым объектом нашего приложения, в том числе Context, который живёт, пока сервис не уничтожен системой, или пользователь сам не закрыл его.
В итоге, индикатором перехода приложения в фон будем считать уничтожение объекта OpenGlView. А возврат на передний план, соответственно, создание этого View. Значит, нужно реализовать для этого соответствующий коллбэк:
private val surfaceHolderCallback = object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { viewModel.appInForeground(binding.openGlView) } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} override fun surfaceDestroyed(holder: SurfaceHolder) { viewModel.appInBackground() }}
binding.openGlView.holder.addCallback(surfaceHolderCallback)
Ну и заменяем OpenGlView на Context. Для этого во ViewModel определим нужные методы:
class StartBroadcastViewModel(application: Application) :AndroidViewModel(application) { //... fun appInForeground(openGlView: OpenGlView) { rtmpCamera2?.let { it.replaceView(openGlView) it.startPreview( StreamParameters.resolution.width, StreamParameters.resolution.height ) } } fun appInBackground() { rtmpCamera2?.let { it.stopPreview() it.replaceView(getApplication() as Context) } } //... override fun onCleared() { super.onCleared() rtmpCamera2?.let { if (it.isStreaming) { _streamState.value = StreamState.STOP it.stopStream() } it.stopPreview() } }}
Также нужно остановить трансляцию при уничтожении ViewModel.
Приостановка трансляции
К сожалению, в библиотеке rtmp-rtsp-stream-client-java не реализована функция приостановки стриминга с сохранением соединения с сервером. Приходится останавливать трансляцию и заново стартовать, а это приводит к лишним задержкам. Чтобы решить эту проблему, я решил имитировать приостановку трансляции отключением камеры и микрофона. Эти функции в библиотеке как раз были доступны.
В этом случае соединение с сервером не обрывается, и задержка при возобновлении трансляции не превышает 8 секунд (стандартная задержка в трансляциях). При этом битрейт при имитации снижается до 70-80 Кбит/с, а значит лишний интернет-трафик практически не расходуется.
//...fun resumeStream() { rtmpCamera2?.let { it.enableAudio() it.glInterface.unMuteVideo() _streamState.value = StreamState.PLAY }}fun pauseStream() { rtmpCamera2?.let { it.disableAudio() it.glInterface.muteVideo() _streamState.value = StreamState.PAUSE }}//...
Как видите, реализовать и фоновый стриминг, и приостановку довольно просто. И rtmp-rtsp-stream-client-java даёт для этого все возможности.