Интро

Недавно получил интересную задачу в работу, сделать приложение для видео-стриминга, это для стартапа 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. Но такое решение имеет свои минусы:

  1. Для начала стрима, стримеру нужно зайти в админку, скопировать ссылку, а дальше в приложении LarixBroadcaster зайти в настройки соединений, и добавить эту ссылку. Стоит понимать, что стримеры это люди, которые не хотят лазить в настройки, предпочтительно нажать одну кнопку и стартовать стрим.

  2. Что бы смотреть чат, нужно рядом держать ноутбук, и с него читать что пишут зрители.

  3. Сложности с проведением тестовых эфиров.

  4. Куча настроек в приложении, в которых стримеру сложно и лень разбираться (битрейт, фпс, кодеки).

Что мы хотим от нашего приложения:

  • Видеть список запланированных стримов

  • Подготовка к проведению стрима (проверить камеру, микрофон)

  • 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 строк).

А какие-то более глобальные выводы можно будет сделать только после того, как обкатаем сборку на большем количестве стримов.