Интро
Недавно получил интересную задачу в работу, сделать приложение для видео-стриминга, это для стартапа ShopStory (ecomm live streaming). Первую версию приложения реализовал используя Open Source библиотеку для стриминга по RTMP HaishinKit. А вторую версию на Larix SDK. В этой статье разберу какие проблемы возникали в процессе.
Требования
Сервис ShopStory.live - одна из первых в России B2B платформ для запуска продаж через live видеоэфиры на сайте e-commerce, которая сотрудничает с крупными ритейлерами, организуя стримы и предоставляя партнёрам удобные инструменты для привлечения новых покупателей и увеличения продаж. Платформа позволяет вести прямые эфиры на сайте клиента в удобном интерфейсе, что позволяет зрителям, находясь дома, быть ближе к товару, которые подробно презентует инфлюенсер. Клиентами ShopStory.live становятся известные бренды декоративной и уходовой косметики, маркетплейсы и небольшие компании, которые в основном представляют beauty-сегмент и хотят быть в тренде и использовать формат live 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 строк).
А какие-то более глобальные выводы можно будет сделать только после того, как обкатаем сборку на большем количестве стримов.
Alexufo
rtmp? хм. удивили. Почему не webrtc?
Раз уж вы платите за sdk, есть, например, православный voximplant. Тоже с русской поддержкой относительно адекватной, довольно быстро дали разраба на мое issue.
Не хотите, поднимайте webrtc на своих мощностях на беслатных либах. Никогда бы в сторону rtmp не глянул бы. Он же в браузерах был жив вместе с флешем. Как вы решаете вопрос звонка с компа в приложение?
Мне довольно просто удалось реализовать звонок с браузера на браузер, с поддержкой адаптивности верстки для мобильных устройств. А уж если свое приложение, то с наличием доп либ совместимость протокола вообше не стоит.
Вы не написали самое главное, какая задержка сигнала вас устраивает? Это кардинально меняет выбор решений. rtmp на память около 3 секунд задерживает сигнал. Если живое общение с менеджером, то webrtc может дать ее в пол секунды. Правда слабовата картинка будет, лучше 2, а если и 10 секунд устраивает, а в обратку менеджеру только чат от клиента, то можно пойти проще и вещать на http протоколах по dash low latency. Задержка 3 секунды.
Посоветую свою статью про стриминг habr.com/ru/post/532424
DmitriiSpace Автор
В статье, которую вы скинули стриминг из OBS идет по RTMP или я что-то неправильно понял?
Предполагаю что Вы о доставке видео до зрителей? Но это тогда не связано с тем как реализована доставка контента до серверов.
Alexufo
да, до сервера стрим идет по rtmp, но как я понимаю, в вашем решении rtmp используется для раздачи и до клиентов? В этом случае невозможно использовать это решение в браузерах. Я не знаю специфику вашего проекта, может быть нужно обязательно загонять людей в приложение, но браузеры я бы точно не обделял вниманием.
Адаптивного кодека, снижающего битрейт при плохом коннекте я как-то не видел. сдается мне простая вещь — это нереально масштабировать. Лучше стримить на три урла с разным битрейтом.
DmitriiSpace Автор
Не, rtmp для раздачи не используется.
Для доставки контента используем решения от Mux «We use RTMP for accepting live broadcasts, and HLS for output streams. This gives your application the ability to stream from a mobile app, broadcast software, or hardware encoder and broadcast to any device.»
Alexufo
значит задержка секунд в 20. А вот low latency HLS не так давно появился. Позволяет сократить до 3.
Aquary
LL HLS пока ещё на ранней стадии. Его на проигрывание поддерживает только платформы от Apple плюс THEO Player. На стороне серверов и сервисов оно только начинает добавляться.
У нас LL HLS реализован в Nimble Streamer, а большинство вендоров к нему только присматриваются — технология не самая тривиальная в реализации.
Aquary
WebRTC очень хорош для видеозвонков, чатов, отдачи потока из браузера, но в данном случае юзкейс — это вещание «в одну сторону», задержка там важна не настолько, как возможность сформировать хорошую картинку с мобильного устройства (на них сейчас камеры приближаются по качеству к профессиональным) и раздать её большому числу зрителей (на самых разных устройствах).
Поэтому пайплайн автора — на базе RTMP с отдачей в облако и раздачей по HLS — отлично отработанный подход, я бы даже сказал — классика.
Alexufo
Картинка страдает при полусекундной задержке, по моим тестам задержка энкодера более чем в 3 секунды не дает выигрыша в качестве. В случае hls задержка в 20 секунд вызвана скорее необходимостью кеширования из-за транспортного протокола. По мне 20 секунд очень не удобно для общения с чатом. Я бы выбрал ll-dash-js (под него есть онлайн) и webrtc — это покрывает массу платформ.
Aquary
Настройки энкодера (key frame interval, например), размер буфера энкодера, величина чанков, размер буфера плеера и отставание при проигрывании — взаимосвязанные вещи, но это не одно и то же. Картинка может страдать от проблем с потерей пакетов, и чем больше буфер, тем проще это митигиролвать. 20 секунд — да, это большая задержка, но таков формат HLS. Нужно больше интерактива — да, можно взять WebRTC, или технологии вроде нашего SLDP для доставки с низкой задежки.
WebRTC — отличная технология, но она — не панацея для всех юзкейсов, как и LL DASH.