Прямые трансляции с мобильных устройств позволяют поддерживать связь с аудиторией, где бы вы ни находились. Сервисы, которые предоставляют такую возможность, пользуются огромной популярностью и применяются в самых разных сферах.
В прошлый раз я рассказывал, как сделать приложение для показа VOD. А сегодня поделюсь, как с помощью опенсорс-инструментов сделать полноценный сервис для онлайн-стриминга на Android и интегрировать его с EdgeStreaming.
Выбираем протоколы для потоковой передачи видео
Проведение онлайн-трансляции можно разделить на 3 основных этапа:
-
Захват видео с камеры и передача на обработку.
-
Обработка на стриминговой платформе (транскодирование под разные устройства и битрейт).
-
Отдача конечным пользователям.
Чтобы передавать стрим с камеры на обработку, а потом отправлять его конечным зрителям, нужно сначала определиться, какие протоколы потоковой передачи для этого будут использоваться.
Для передачи видео с камеры мы будем использовать RTMP. Его поддерживают большинство стриминговых платформ. При этом у него достаточно низкие задержки и есть ретрансляция пакетов данных на основе TCP, что делает его очень надёжным.
Для доставки конечным зрителям есть 2 самых популярных протокола — HLS и MPEG-DASH. iOS и Android оснащены нативными плеерами, AVPlayer и MediaPlayer соответственно, которые поддерживают воспроизведение HLS, поэтому остановимся именно на этом протоколе.
Выираем библиотеку для формирования RTMP-потока
Опенсорс-решений для RTMP-стриминга с Android довольно мало, а действительно рабочих ещё меньше. Вот некоторые из них.
1. HashinKit
Плюсы:
-
Стабильно обновляется
-
Воспроизводит RTMP
-
Использует актуальный Camera2 API
-
Умеет переключать камеру во время трансляции
-
Даёт возможность настроить параметры трансляции
Минусы:
-
Отсутствует возможность использования адаптивного битрейта
-
Отсутствует функция приостановки трансляции
Плюсы:
-
Поддерживает минимальный API 28
-
Умеет включать и выключать звук во время трансляции
Минусы:
-
Устарела — последний коммит был 28 июля 2020
-
Использует устаревший Camera API
-
Отсутствует импорт библиотеки через gradle
-
Фреймы, отправляемые библиотекой, содержат ошибки (DC, AC, MV)
-
Сложная в реализации (сложнее, чем некоторые другие библиотеки)
3. rtmp-rtsp-stream-client-java
Плюсы:
-
Свежая библиотека, стабильно обновляется
-
Поддерживает минимальный API 16
-
Поддерживает Camera и Camera2 API
-
Умеет переключать камеры во время трансляции
-
Поддерживает адаптивный битрейт
-
Умеет включать и выключать звук и видео во время трансляции
-
Даёт возможность настроить параметры трансляции
-
Позволяет устанавливать OpenGL-фильтры, изображений, GIF или текста в реальном времени
-
Очень простая в использовании, всё работает «из коробки»
-
Есть документация и инструкции к библиотеке на GitHub
-
Усть удобный импорт библиотеки через gradle
Минусы:
-
Нет функции приостановки трансляции
Как видите, больше всего плюсов и меньше всего минусов у rtmp-rtsp-stream-client-java. Поэтому именно эту библиотеку мы и будем использовать.
Реализуем стриминг по RTMP с Android-смартфона
Итак, на первом этапе нам надо сделать так, чтобы наше Android-устройство могло снимать видео и в реальном времени передавать его на обработку по RTMP.
В библиотеке rtmp-rtsp-stream-client-java есть два типа объектов для стриминга — RtmpCamera1 и RtmpCamera2. Для захвата потока с камеры смартфона в них используются RtmpCamera API и RtmpCamera2 API соответственно. RtmpCamera API устарел, начиная с Android API level 21. Поэтому, естественно, мы будем использовать RtmpCamera2.
Сейчас мы пошагово рассмотрим, как rtmp-rtsp-stream-client-java поможет в мобильном стриминге. Но сначала давайте кратко разберём схему её работы.
-
При вызове метода rtmpCamera2.startPreview() происходит захват камеры.
-
Захваченная сессия поступает на вход OpenGlView, где показывается пользователю.
-
При вызове метода rtmpCamera2.startStream() устанавливается соединение с удаленным сервером.
-
Захваченная сессия передаётся по RTMP-протоколу на указанный rtmpUrl.
1. Инициализация
Чтобы использовать библиотеку rtmp-rtsp-stream-client-java в своём проекте, надо добавить зависимости в build.gradle:
allprojects { repositories { maven { url 'https://jitpack.io' } } } dependencies { implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.1.7' }
2. Разрешения
Дальше нам нужно указать необходимые разрешения в файле AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
3. Настройка View для отображения захвата с камеры
При стриминге со смартфона надо видеть, что транслируется. Для этого есть соответствующий View, который будет отображать поток с камеры на экране. В Android для этих целей используется SurfaceView или TextureView. Ещё в библиотеке реализована собственная OpenGlView, которая наследуется от SurfaceView.
С RtmpCamera можно использовать любой из этих View. С RtmpCamera2, к сожалению, доступен только OpenGlView. Но среди них только он позволяет включать в стриминг разные фильтры, изображения, GIF или текст.
Добавляем OpenGlView к Layout Activity или Fragment-а, чтобы видеть поток с камеры:
<com.pedro.rtplibrary.view.OpenGlView android:id="@+id/openGlView" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true" android:visibility="gone" app:keepAspectRatio="true" app:aspectRatioMode="adjust"/>
4. Подготовка к стримингу
Чтобы инициализировать объект RtmpCamera2, нужен объект OpenGlView и реализация интерфейса ConnectCheckerRtmp:
private val connectCheckerRtmp = object : ConnectCheckerRtmp { override fun onAuthErrorRtmp() { _toastMessageId.postValue(R.string.auth_error) } override fun onAuthSuccessRtmp() { _toastMessageId.postValue(R.string.auth_success) } override fun onConnectionFailedRtmp(reason: String) { _toastMessageId.postValue(R.string.connection_failed) stopBroadcast() } override fun onConnectionStartedRtmp(rtmpUrl: String) {} override fun onConnectionSuccessRtmp() { _toastMessageId.postValue(R.string.connection_success) _streamState.postValue(StreamState.PLAY) } override fun onDisconnectRtmp() { _toastMessageId.postValue(R.string.disconnected) _streamState.postValue(StreamState.STOP) } override fun onNewBitrateRtmp(bitrate: Long) {} }
Для использования адаптивного битрейта вносим дополнения в реализацию этого интерфейса:
//... private lateinit var bitrateAdapter: BitrateAdapter override fun onConnectionSuccessRtmp() { bitrateAdapter = BitrateAdapter { bitrate -> rtmpCamera2?.setVideoBitrateOnFly(bitrate) }.apply { setMaxBitrate(StreamParameters.maxBitrate) } _toastMessageId.postValue(R.string.connection_success) _currentBitrate.postValue(rtmpCamera2?.bitrate) _streamState.postValue(StreamState.PLAY) } //... override fun onNewBitrateRtmp(bitrate: Long) { bitrateAdapter.adaptBitrate(bitrate) _currentBitrate.postValue(bitrate.toInt()) disableStreamingAfterTimeOut() }
В объект OpenGlView добавляем коллбэк, в соответствующих методах которого будем запускать и останавливать превью с камеры:
private val surfaceHolderCallback = object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { rtmpCamera2?.startPreview( StreamParameters.resolution.width, StreamParameters.resolution.height ) } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} override fun surfaceDestroyed(holder: SurfaceHolder) { rtmpCamera2?.stopPreview() } }
binding.openGlView.holder.addCallback(surfaceHolderCallback)
Создаём объект RtmpCamera2, с помощью которого будет осуществляться стриминг:
rtmpCamera2 = RtmpCamera2(openGlView, connectCheckerRtmp)
5. Запуск и остановка трансляции
Задаём параметры видео и аудио, запускаем трансляцию.
Стриминг с параметрами по умолчанию:
fun startBroadcast(rtmpUrl: String) { rtmpCamera2?.let { if (!it.isStreaming) { if (it.prepareAudio() && it.prepareVideo()) { _streamState.value = StreamState.PLAY it.startStream(rtmpU) } else { _streamState.value = StreamState.STOP _toastMessageId.value = R.string.error_preparing_stream } } } }
Стрим с кастомными параметрами:
fun startBroadcast(rtmpUrl: String) { val audioIsReady = rtmpCamera2.prepareAudio( StreamParameters.audioBitrate, StreamParameters.sampleRate, StreamParameters.isStereo, StreamParameters.echoCanceler, StreamParameters.noiseSuppressor ) val videoIsReady = rtmpCamera2.prepareVideo( StreamParameters.resolution.width, StreamParameters.resolution.height, StreamParameters.fps, StreamParameters.startBitrate, StreamParameters.iFrameIntervalInSeconds, CameraHelper.getCameraOrientation(getApplication()) ) rtmpCamera2?.let { if (!it.isStreaming) { if (audioIsReady && videoIsReady) { _streamState.value = StreamState.PLAY it.startStream(rtmpUrl) } else { _streamState.value = StreamState.STOP _toastMessageId.value = R.string.error_preparing_stream } } } }
Останавливаем эфир:
fun stopBroadcast() { rtmpCamera2?.let { if (it.isStreaming) { _streamState.value = StreamState.STOP it.stopStream() } } }
Интегрируемся со стриминговой платформой EdgeЦентр
Обрабатывать полученный с камеры стрим и отправлять его пользователям мы будем с помощью EdgeStreaming. В прошлой статье я уже упоминал, что у платформы есть пробный период 14 дней. За это время можно провести 3 независимые трансляции общей продолжительностью до 20 минут. Это не много, но будет достаточно, чтобы понять, как работает платформа, без лишних вложений.
Для взаимодействия с EdgeStreaming понадобится EdgeЦентр API. Запросы будем выполнять с помощью Retrofit совместно с RxJava. Но можно выбрать и любой другой способ отправки HTTP-запросов.
Авторизация
Для работы с API нужно авторизоваться. Нам нужно получить Access Token — для этого используем email и пароль, которые указали при регистрации на платформе. Токен мы будем использовать в последующих запросах.
class AuthRequestBody( @SerializedName("username") val eMail: String, @SerializedName("password") val password: String, @SerializedName("one_time_password") val oneTimePassword: String = "authenticator passcode" ) interface AuthApi { @POST("./iam/auth/jwt/login") fun performLogin(@Body body: AuthRequestBody): Single<AuthResponse> } private fun auth(eMail: String, password: String) { val requestBody = AuthRequestBody(eMail = eMail, password = password) compositeDisposable.add( (requireActivity().application as EdgeApp).authApi .performLogin(requestBody) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .auth(requireActivity(), requestBody) .subscribe({ authResponse -> showToast(R.string.logged_success) saveAuthData(requestBody, authResponse) routeToStreams() }, { showToast(R.string.logged_fail) }) ) }
Получение PUSH URL
Получить URL для отправки RTMP-потока можно двумя способами:
Способ 1. Отправить запрос Get all live streams для получения всех live-потоков. В ответе мы получим данные по всем созданным в аккаунте видеопотокам.
Пример отправки запроса:
interface StreamsApi { /** * @param page integer; Query parameter. Use it to list the paginated content * @param with_broadcasts integer; Query parameter. * Set to 1 to get details of the broadcasts associated with the stream */ @GET("./streaming/streams") fun getStreams( @Header("Authorization") accessToken: String, @Query("page") page: Int, @Query("with_broadcasts") withBroadcasts: Int = 1 ): Single<List<StreamItemResponse>> //... }
private fun loadStreamItems(page: Int = 1) { val accessToken = getAccessToken() var currentPage = page if (currentPage == 1) { streamItems.clear() } compositeDisposable.add( (requireActivity().application as EdgeApp).streamsApi .getStreams("Bearer $accessToken", page = currentPage) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ if (it.isNotEmpty()) { it.forEach { streamItemResponse -> streamItems.add(StreamItemModel.getInstance(streamItemResponse)) } loadStreamItems(page = ++currentPage) } else { updateDataInAdapter() } }, { it.printStackTrace() }) ) }
Способ 2. Отправить Get live stream для получения конкретного live-потока. В ответе мы получим информацию только о запрошенном видеопотоке, если такой существует.
Пример:
interface StreamsApi { //... @GET("./streaming/streams/{stream_id}") fun getStreamDetailed( @Header("Authorization") accessToken: String, @Path("stream_id") streamId: Int ): Single<StreamDetailedResponse> //... }
private fun getStreamDetailedInfo(streamId: Int) { val accessToken = getAccessToken() compositeDisposable.add( (requireActivity().application as EdgeApp).streamsApi .getStreamDetailed("Bearer $accessToken", streamId) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ streamDetailedInfo -> //... }, { it.printStackTrace() }) ) }
Из ответов получаем push_url и берём его в качестве URL для отправки RTMP-потока. После начала стрима трансляция запустится автоматически. Выбираем нужный поток в личном кабинете. Можно воспользоваться предварительным просмотром, прежде чем публиковать стрим на свой сайт.
Воспроизводим трансляцию в плеере
С помощью EdgeStreaming можно показать стрим на сторонних ресурсах (в том числе в социальных сетях) в разных форматах, в том числе в HLS. Одновременную запись и проигрывание на одном устройстве мы в нашем примере рассматривать не будем. Стриминг должен быть запущен с любого другого устройства.
Для воспроизведения активного стрима берём стандартный MediaPlayer Android. Если вы хотите получить больше контроля над видеопотоком и возможность кастомизации, мы рекомендуем обратить внимание на ExoPlayer.
Настройка View для отображения видеопотока на экране
Здесь нам понадобится VideoView. Добавляем его в Layout Activity или Fragment-a, в котором планируется проигрывание:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@color/black" android:id="@+id/streamPlayer" tools:context=".screens.StreamPlayerFragment"> <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center"/> ... </FrameLayout>
Воспроизведение
Перед запуском воспроизведения встраиваем hls_playlist_url активной трансляции в проигрыватель при его инициализации. hls_playlist_url возвращается в ответе на Get live stream, о котором я говорил говорили выше.
Инициализация плеера:
private fun initializePlayer(streamUri: String) { val videoView = binding.videoView videoView.setVideoURI(Uri.parse(streamUri)) val mediaController = MediaController(videoView.context) mediaController.setMediaPlayer(videoView) videoView.setMediaController(mediaController) videoView.setOnPreparedListener { binding.progressBar.visibility = View.GONE videoView.start() } videoView.setOnErrorListener(mediaPlayerOnErrorListener) }
Как только инициализация завершится, запускаем стрим вызовом метода videoView.start().
Запуск плеера:
private fun releasePlayer() { binding.videoView.stopPlayback() }
Подведём итоги
Как видите, сделать приложение для трансляций на Android — не так сложно. С помощью нашей стриминговой платформы и опенсорса всё можно сделать довольно быстро и без лишней головной боли.
Если хотите подробнее изучить проект, исходный код получившегося приложения я выложил на GitHub.
ссылка на оригинал статьи https://habr.com/ru/articles/766558/
Добавить комментарий