Долгий запуск, буферизация видео, большие задержки, прерывания трансляции и прочие лаги — типичные проблемы при разработке приложений для стриминга и прямых трансляций. Тот, кто хоть раз разрабатывал такие сервисы, обязательно сталкивался хотя бы с одной из них.
В предыдущих статьях мы рассказывали, как разработать приложения для стриминга на iOS и Android. А сегодня поделимся, с какими проблемами мы столкнулись в процессе и как их решали.
Всё, что требуется от мобильного приложения, — это захватить видео и аудио с камеры, сформировать из них поток данных и отправить зрителям. Для массового распространения контента на широкую аудиторию потребуется стриминговая платформа.
Единственный минус стриминговой платформы — задержка. Трансляции — довольно сложный, комплексный процесс. Определённая задержка возникает на каждом этапе.
Наши разработчики смогли собрать стабильное, функциональное и быстрое решение, которому требуется 5 секунд для запуска всех процессов, а end-to-end-задержка при вещании в режиме Low latency занимает 4 секунды.
В таблице ниже — несколько платформ, которые по-разному решают задачу уменьшения задержек. Мы сравнили несколько решений, изучили каждое и нашли оптимальный подход.
Запустить трансляцию на стриминговой платформе EdgeЦентр можно за 5 минут. Но для этого у вас должен быть аккаунт. Зарегистрироваться можно, написав нашим менеджерам. У вас будет 14 дней бесплатного пробного периода, чтобы протестировать решение.
Все участвующие в стриминге процессы неразрывно связаны друг с другом. Внесение изменений в один влияет на все последующие. Поэтому разделять их на отдельные блоки будет некорректно. Мы рассмотрим, что и как можно оптимизировать.
Чтобы начать декодирование и обработку любого видеопотока, нужен iframe. Мы провели тесты и по их результатам выбрали для своих приложений оптимальный 2-секундный интервал I-кадров. Но в некоторых случаях его можно изменить на 1 секунду. За счёт уменьшения длины GOP декодирование, а значит, и начало обработки потока происходит быстрее.
Выставляем maxKeyFrameIntervalDuration = 2.
Выставляем iFrameIntervalInSeconds = 2.
Если во время стриминга нужны короткие паузы, например для переключения на другое приложение, стриминг можно продолжить в фоне и сохранить целостность видео. При этом мы не тратим время на инициализацию всех процессов и сохраняем минимальную end-to-end-задержку при возвращении в эфир.
Apple запрещает записывать видео в свёрнутом приложении, поэтому первоначальным решением было отключение камеры в соответствующий момент и подключение её при возвращении в эфир. Для этого мы подписались на оповещение от системы о входе/выходе из background state.
Это не сработало. Соединение не разрывалось, но библиотека не отправляла видео RTMP-потока. Поэтому мы решили внести правки в саму библиотеку.
Каждый раз, когда система отправляет буфер с аудио в AVCaptureAudioDataOutputSampleBufferDelegate, проверяется, отключены ли от сессии все устройства. Подключённым должен оставаться только микрофон. Если всё корректно, создаётся timingInfo. Он несёт информацию о duration, dts и pts фрагмента.
После этого вызывается метод pushPauseImageIntoVideoStream класса AVMixer, в котором идёт проверка на наличие картинки для паузы. А далее через метод pixelBufferFromCGImage создаётся CVPixelBuffer c данными картинки и через метод createBuffer — сам CMSampleBuffer, который отправляется в AVCaptureVideoDataOutputSampleBufferDelegate.
Расширение для AVMixer:
В класс AVMixer добавляем свойство pauseImage:
В AVAudioIOUnit добавляем функционал в метод func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection):
С Android всё оказалось проще. Если заглянуть глубже в исходный код библиотеки, которую мы использовали, становится понятно, что стриминг, на самом деле, находится в отдельном потоке.
Учитывая жизненный цикл компонента, в котором у нас инициализируется трансляция, мы решили инициализировать его во ViewModel — он остаётся живым на протяжении всех жизненных процессов компонента, к которому привязан (Activity, Fragment).
В жизненном цикле ViewModel ничего не изменится, даже если произойдёт смена конфигурации, ориентации, переход в фон и так далее.
Но небольшая проблема всё-таки есть. Для стриминга нам нужно создать объект RtmpCamera2(), который зависит от объекта OpenGlView. Это элемент UI, а значит, он уничтожится при переходе приложения в фон, и трансляция прервётся.
Решение нашлось быстро. В библиотеке предусмотрена возможность заменять «на лету» View объекта RtmpCamera2. Мы можем заменить его объектом Context нашего приложения. А он живёт, пока приложение не уничтожено системой или пользователь сам его не закрыл.
Считаем индикатором перехода приложения в фон уничтожение объекта OpenGlView, а сигналом к возврату на передний план будет, соответственно, создание этого View. Нам нужно реализовать для этого соответствующий колбэк:
Дальше, как мы уже сказали, нужна замена объекта OpenGlView на Context при переходе в фон и обратно. Для этого во ViewModel мы определяем нужные методы. А ещё нам потребуется остановить трансляцию при уничтожении ViewModel.
А если нам нужно поставить трансляцию на паузу без перехода в фон, мы просто отключаем камеру и микрофон. Битрейт в таком режиме снижается до 70–80 Кбит/с, что даёт возможность не тратить лишний трафик.
Чтобы вовремя получить информацию о готовности контента к воспроизведению и мгновенно запустить трансляцию, используем WebSocket:
Если мы вещаем с мобильного устройства, значит, для передачи видео будут использоваться сотовые сети. В мобильном стриминге это главная проблема: уровень и качество сигнала зависят от множества факторов. Поэтому нужно обязательно адаптировать битрейт и разрешение под доступную полосу. Это поможет поддерживать стабильную трансляцию при любом качестве интернета у зрителей.
Для реализации адаптивного битрейта используется два метода RTMPStreamDelegate:
Пример реализации:
Адаптивное разрешение настраиваем по соотношению с битрейтом. За основу мы взяли вот такое соотношение:
Разрешение | 1920×1080 | 1280×720 | 854×480 | 640×360 |
Битрейт видео | 6 Мбит/c | 2 Мбит/c | 0,8 Мбит/c | 0,4 Мбит/c |
Если полоса пропускания падает больше чем на половину разницы между двумя соседними разрешениями, переключаем разрешение на более низкое. А при повышении битрейта, соответственно, меняем разрешение на более высокое.
Здесь для использования адаптивного битрейта нужно изменить реализацию интерфейса ConnectCheckerRtmp:
Организовать стриминг с мобильных устройств — не такая уж сложная задача. С помощью открытого исходного кода и нашей стриминговой платформы сделать это можно быстро и с минимальными финансовыми затратами.
Конечно, проблемы при разработке возникают всегда. Мы надеемся, что наши решения помогут вам упростить этот процесс и решить поставленные задачи быстрее.
Подробнее о разработке приложений для стриминга на iOS и Android читайте в наших статьях:
Запускайте трансляции на мобильных устройствах без лишних проблем с помощью нашей стриминговой платформы.