Есливы решили сделать собственное приложение для стриминга на Android, при разработке нужно учесть множество разных нюансов. Например, зрители могут свернуть вашу трансляцию в процессе просмотра, а через какое-то время вернуться обратно. Как должно при этом работать приложение? Должна ли трансляция приостановиться или идти фоном?

Какое именно поведение реализовать в подобной ситуации — решать вам. Но очень важно предусмотреть подобные моменты технически, чтобы при сворачивании всё работало так, как вы задумали.

Этот материал — продолжение моей предыдущей статьи про создание мобильного приложения для стриминга на Android. В ней я рассказывал о базовых моментах разработки. А сейчас поговорим о нюансах. Расскажу, как технически реализовать приостановку трансляции и фоновый стриминг на Android с помощью опенсорс-библиотеки rtmp-rtsp-stream-client-java.

Фоновый стриминг

Сначала разберём кейс, когда приложение переходит в фон и обратно на передний план. Если заглянуть чуть глубже в исходный код rtmp-rtsp-stream-client-java, станет понятно, что стриминг сам по себе проходит в отдельном потоке:

package com.pedro.rtmp.rtmp
class 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 даёт для этого все возможности.

Комментарии (0)