Прямые трансляции с мобильных устройств позволяют поддерживать связь с аудиторией, где бы вы ни находились. Сервисы, которые предоставляют такую возможность, пользуются огромной популярностью и применяются в самых разных сферах.
В прошлый раз я рассказывал, как сделать приложение для показа 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.