Как реализовать приостановку трансляции и фоновый стриминг на Android с помощью опенсорс-библиотеки

от автора

Если вы решили сделать собственное приложение для стриминга на 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 даёт для этого все возможности.


ссылка на оригинал статьи https://habr.com/ru/articles/771276/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *