Видео-стриминг на iOS
Интро
Недавно получил интересную задачу в работу, сделать приложение для видео-стриминга, это для стартапа ShopStory (ecomm live streaming). Первую версию приложения реализовал используя Open Source библиотеку для стриминга по RTMP HaishinKit. А вторую версию на Larix SDK. В этой статье разберу какие проблемы возникали в процессе.
Требования
Сервис ShopStory.live — одна из первых в России B2B платформ для запуска продаж через live видеоэфиры на сайте e-commerce. Платформа позволяет вести прямые эфиры на сайте клиента в удобном интерфейсе, что позволяет зрителям, находясь дома, быть ближе к товару, которые подробно презентует инфлюенсер.
До разработки своего приложения в ShopStory, стримеры использовали LarixBroadcaster для проведения стримов, это бесплатное приложение для Android и iOS. Но такое решение имеет свои минусы:
Для начала стрима, стримеру нужно зайти в админку, скопировать ссылку, а дальше в приложении
LarixBroadcaster
зайти в настройки соединений, и добавить эту ссылку. Стоит понимать, что стримеры это люди, которые не хотят лазить в настройки, предпочтительно нажать одну кнопку и стартовать стрим.Что бы смотреть чат, нужно рядом держать ноутбук, и с него читать что пишут зрители.
Сложности с проведением тестовых эфиров.
Куча настроек в приложении, в которых стримеру сложно и лень разбираться (битрейт, фпс, кодеки).
Что мы хотим от нашего приложения:
Видеть список запланированных стримов
Подготовка к проведению стрима (проверить камеру, микрофон)
ABR — Adaptive BitRate (при плохом соединении уметь снижать качество)
Готовые настройки битрейта, fps, энкодинга и т.п.
Простота для стримера при запуске.
В данной статье речь будет только про самую важную часть приложения — стриминг. Larix SDK
— платный, поэтому сначала внедряем и используем бесплатную библиотеку.
Список бесплатных, которые рассматривали:
LFLiveKit — 4.2k звезд на гитхабе, последний коммит в 2016 г. 115 issue, которые не решаются.
HaishinKit — 2.1k звезд на гитхабе, на момент написания последний коммит 7 мая. 11 issues.
VideoCore — 1.5k звезд на гитхабе, последний коммит 2015 г. Не поддерживается.
KSY Live iOS SDK — 0.8k звезд на гитхабе, последний коммит 22 марта 2020. Весь README на китайском.
Остановились на внедрении HaishinKit. Если у вас есть на примете хорошие варианты, велком в комментарии, поделитесь какие там есть плюсы и минусы.
HaishinKit
Понятная документация, внедрение супер простое. Данная библиотека забирает на себя максимум. Разработчику не нужно заботиться о работе с камерой/микрофоном, эта либа делает всё за тебя. Никаких AVCaptureSession, AVCaptureDevice, AVCaptureDeviceInput
и тому подобное. Просто создаем View
, делаем attach
к RTMPStream
.
Накидаем протокол:
protocol BroadcastService: AnyObject {
func connect()
func publish()
func stop()
}
Из документации берем примеры конфигурации и реализуем нужный нам класс.
class HaishinBroadcastService: BroadcastService {}
ABR — Adaptive BitRate
При хорошем соединении передает более высокое качество видео, когда интернет на телефоне начинает болеть, то понижаем качество (битрейт).
Для реализации ABR, берем пример из issue. Имплементим протокол RTMPStreamDelegate
.
extension HaishinBroadcastService: RTMPStreamDelegate {
func rtmpStream(_ stream: RTMPStream, didPublishInsufficientBW connection: RTMPConnection) {
guard self.config.adaptiveBitrate else { return }
guard let bitrate = self.currentBitrate else {
assertionFailure()
return
}
let newBitrate = max(UInt32(Double(bitrate) * Constants.bitrateDown), Constants.minBitrate)
self.rtmpStream.videoSettings[.bitrate] = newBitrate
}
func rtmpStream(_ stream: RTMPStream, didPublishSufficientBW connection: RTMPConnection) {
guard self.config.adaptiveBitrate else { return }
guard let currentBitrate = self.currentBitrate,
currentBitrate < Constants.maxBitrate else {
return
}
guard self.bitrateRetryCounter >= Constants.retrySecBeforeUpBitrate else {
self.bitrateRetryCounter += 1
return
}
self.bitrateRetryCounter = 0
let newBitrate = min(Constants.maxBitrate, UInt32(Double(currentBitrate) * Constants.bitrateUp))
if newBitrate == currentBitrate { return }
self.rtmpStream.videoSettings[.bitrate] = newBitrate
}
}
private struct Constants {
static let bitrateDown: Double = 0.75
static let bitrateUp: Double = 1.15
static let retrySecBeforeUpBitrate = 20
}
В отличии от варианта из issue — опускаем битрейт постепенно (там просто сразу в 2 раза уменьшается), поднимаем тоже битрейт постепенно. Метод didPublishInsufficientBW
вызывается в случаях, когда библиотека не может отправить все фреймы стрима.
Опытным путём остановились на таких константах:
если либа не может отправить все фреймы, мы снижаем битрейт умножая текущий на 0.75
если успешно отправились фреймы, то через 20 сек (эти методы делегата работают по таймеру в самой либе), пытаемся поднять битрейт умножая на 1.15
Live update resolution
Так же при падении качества соединения на телефоне стримера, сделали попытку изменения разрешения стрима, но это не увенчалось успехом. RTMP не поддерживает изменение разрешения на лету. Посмотрели как сделано в VK Live и там они разрывают соединение при изменении разрешения. В Instagram смогли это реализовать, вероятно есть разные rtmp ссылки, для разного качества и при снижении скорости интернета, начинается стрим в другую ссылку, а бэкенд уже это склеивает и обрабатывает (это лишь догадки, глубокого исследования не проводили). В ShopStory возможно реализуем позже.
Графики
После проведения ряда стримов периодически наблюдаем странные падения. Это происходит как на Wi-Fi, так и на LTE. Решили пробовать платное решение Larix SDK
. Потому что при использовании LarixBroadcaster — подобное не происходило.
Larix SDK
При покупке тебе предоставляют архив с исходниками LarixBroadcaster
+ LarixDemo (упрощенный вариант), общую диаграмму архитектуры и описание компонентов, StepByStepGuide.
Плюсы:
Широко используется в крупных компаниях, и в некотором роде стандарт для стриминга
Есть живая русскоговорящая тех. поддержка с экспертизой в стриминге
Минусы:
платное
документация очень скудная, если хочешь что-то сделать изучай код
LarixBroadcaster
(я люблю почитать исходники, но не в этом случае: over 2000 строк на файл)нет дисконнекта когда теряется соединение с интернетом
нет отличий в
connect
иpublish
Изучай код если хочешь что-то сделать
Оооо… это отдельная боль, в LarixBroadcaster
пришлось изучать ViewController
на 2100 строк, и еще один важный класс Streamer
на 1100 строк. Не ожидал я такого от платной SDK. Ок… Для меня было загадкой, почему они не добавили это всё в кишки библиотеки. Получил комментарий от @Aquary (приглашаю в комментарии):
»Изначально мы всю логику закрыли именно «в кишки библиотеки». Но жизнь оказалась разнообразнее — нас постоянно просили добавить что-то ещё по мере выхода новых фич. Так что в итоге в библиотеке осталась часть, связанная с работой протоколов. Остальное — исходники. Как показывает практика, клиентам такой подход ближе, т.к. нет почти никаких ограничений на реализацию со стороны нас, как разработчиков.»
На мой взгляд могли бы, для этого дать из SDK понятный интерфейс и закрыть это всё протоколами для возможности расширения и своих кастомизаций. Здесь же бери исходники, вытаскивай нужное и тащи к себе в проект. Таким образом для переезда c HaishinKit
нужно писать код для работы с камерой, микрофоном и т.д. (ранее это было всё скрыто в HaishinKit
).
Такая же проблема и с ABR, я ожидал (ваши ожидания ваша проблема), что это будет встроено в либу, и просто задав один параметр можешь включить адаптивный битрейт. Но это не так. В LarixBroadcaster
есть просто 3 класса StreamConditionerMode1, 2, 3,
которые реализуют логику. Хочешь себе в проект ABR? Тащи еще к себе в проект эти классы или пиши свою реализацию ABR на основе этих исходников (это и плюс и минус).
Нет дисконнекта
Странно, но это так. Если на телефоне пропадёт соединение, то в метод делегата ты не получаешь status = disconnected
. В обращении к тех поддержке ответили, что планируют это реализовать в ближайшее время.
func connectionStateDidChangeId(_ connectionID: Int32, state: ConnectionState, status: ConnectionStatus, info: [AnyHashable: Any]) {}
В Larix
просто будет копиться буффер фреймов для отправки.
Решение: из класса SDK StreamerEngineProxy
можем получить bytesSent
и bytesDelivered
, на основе этих двух методов, можно решать делать реконнект или нет. Если видим, что уже собирается большой буфер, то делаем принудительный дисконнект.
Connect и Publish
По спецификации RTMP, есть отдельные команды publish
и connect
, в Larix
я не нашел (а может этого и нет), как отдельно вызывать эти команды. Из-за этого наш протокол BroadcastService
теперь имеет изъяны.
Для чего нам такая возможность?
Что бы изначально можно было подключиться, посмотреть, что пакеты успешно отправляются без сбоев, возможно, настроить оптимальный битрейт, и только потом начинать стрим.
Для нормальной записи стрима. Всё, что записывается после вызова
publish
, попадет в конечную запись, и нужно будет вырезать, например, начало (подготовку стримера). Если делатьpublish
только после того как стример готов (и поправил свою прическу). То постобработка не нужна.
Графики
Получились более стабильные и ровные после переезда. Есть еще ряд задач, которые нужно докручивать для повышения стабильности и сейчас это самое важное в проекте.
Вывод
Выбор бесплатной библиотеки для стриминга на iOS не очень большой и по сути всё сводится к одному варианту — HaishinKit
. У него есть несомненное преимущество — открытый исходный код и если с Larix
не удастся выровнять графики и повысить стабильность, будем погружаться в open source и искать места, которые можно улучшать.
Покупая платную SDK — не ожидай что она решит все твои проблемы, возможно у тебя их станет больше (изучать vc на over 2000 строк).
А какие-то более глобальные выводы можно будет сделать только после того, как обкатаем сборку на большем количестве стримов.