Реализация лайв-стриминга снимаемого видео в приложении для iOS

Под лайв-стримингом мы понимаем функцию приложения, которая позволяет передавать видео по сети одновременно со съемкой, не дожидаясь полного окончания записи видеофайла.В этой статье мы хотим рассказать о том, как мы подошли к ее решению в ходе разработки Видеокамеры Together для iOS.

Видеокамера Together — это приложение для iPhone, которое позволяет снимать видеоролики, удобно рассортировать собственную видеоколлекцию как автоматически по дате, месту съемки, тэгам, так и вручную по альбомам, синхронизировать коллекцию между устройствами и делиться видео с друзьями, в том числе в формате потокового видео.В самом начале разработки приложение Together задумывалось как приложение для телеконференций, затем акцент сместился к функциональности лайв-стриминга для целей одностороннего вещания на большую аудиторию, как в сервисах LiveStream и Ustream.И, наконец, подойдя к первому публичному релизу, приложение превратилось в менеджер личной библиотеки видеозаписей.

На первый взгляд в лайв-стриминге не должно быть ничего сложного.Существует множество широко известных приложений и сервисов для самых разных платформ, которые используют эту функцию.Для некоторых из них она является основной: Skype, Chatroulette, Livestream, Facetime и многие другие.Однако, средства разработки и стандартные библиотеки для iOS затрудняют оптимальную реализацию этой функции, так как не предоставляют прямого доступа к аппаратному кодированию видео.С точки зрения реализации лайв-стриминг можно разбить на следующие подзадачи:

  • Получение данных видеопотока в процессе съемки.
  • Парсинг данных видеопотока.
  • Преобразование в формат, поддерживаемый сервером.
  • Отправка этих данных на сервер.

Можно выделить следующие основные требования к реализации лайв-стриминга в приложении:

  • Минимальная задержка по времени между съемкой кадра видео и отображением его потребителю.
  • Минимальный объем данных, пересылаемых по сети, при сохранении приемлемого качества изображения и звука.
  • Оптимальное использование ресурсов процессора, памяти и хранилища устройства, на котором производится съемка.
  • Минимальный расход батареи.

В зависимости от целей для которых приложение использует функционал лайв-стриминга, на первый план выходят те или иные требования.Например, требование минимизации расхода батареи противоречит требованию минимизации задержки, так как отправка данных по сети большими кусками более энергоэффективна, чем поддержание постоянного соединения, по которому данные отправляются по мере съемки.Наше приложение Together не ставит цель получения обратной связи от зрителей в реальном времени, в тоже время, мы даем возможность поделиться со своими друзьями снимаемыми кадрами как можно скорее.Поэтому требование минимизации расхода батареи для нас стало первоочередным.iOS SDK включает достаточно богатый набор возможностей по работе с камерой и данными видео и аудио.Подробнее о фреймворках CoreMedia и AVFoundation мы уже писали в предыдущих статьях.Классы AVCaptureAudioDataOutput и AVCaptureVideoDataOutput совместно с AVCaptureSession позволяют получать покадрово информацию с камеры в виде несжатых буферов видео или аудио (CMSampleBuffer).В комплекте с SDK поставляются примеры (AVCamDemo, RosyWriter), которые иллюстрируют работу с этими классами.

Для того, чтобы передать полученные с камеры буферы на сервер, их необходимо предварительно сжать каким-нибудь кодеком.Кодирование видео с хорошим коэфициентом сжатия обычно требует довольно больших затрат процессорного времени и соответственно энергии батареи.Устройства на iOS имеют специальные аппаратные средства для быстрого сжатия видео кодеком H.264.Для того, чтобы разработчики приложений могли их использовать, в SDK предусмотрено всего два класса, различающихся по простоте использования и доступным возможностям:

  • AVCaptureMovieFileOutput выводит изображение с камеры напрямую в файл MOV или MP4.
  • AVAssetWriter тоже сохраняет видео в файле, но дополнительно дает возможность в качестве исходного изображения использовать кадры, предоставленные разработчиком, которые он может как получить с камеры в виде объектов CMSampleBuffer так и сгенерировать программно.
Рассмотрим затраты процессорного времени на кодирование видео в H.264 при использовании стандартных библиотек SDK и FFmpeg на примере проекта FFmpeg-iOS-Encoder.Сравнение производилось с помощью инструмента Time Profiler во время съемки и одновременной записи в файл видео с разрешением 1920?1080.Сравнение FFmpeg и AVAssetWriterНа первом графике запись видео ведется с помощью класса AVAssetWriter.Нагрузка на процессор практически не отличается до и после начала записи.При этом часть нагрузки по кодированию передается системному процессу mediaserverd, но даже с учетом этого общая нагрузка очень мала и съемка видео производится плавно, без прерываний, со скоростью 30 кадров в секунду.На втором графике запись видео ведется с помощью библиотек FFmpeg с использованием кодека MPEG4 и с понижением разрешения видео до 320?240. При этом процессор загружен практически на 100% и успевает записывать видео только со скоростью 10-15 кадров в секунду.

Для отправки видео по сети идеальным вариантом было бы пропустить этап сохранения данных на диск, а вместо этого сразу получать сжатые данные непосредственно в память приложения.Но, к сожалению, в SDK отсутствует такая возможность, поэтому единственное, что остается — это считывать сжатые данные из файла, в который пишет система.

Внимательный читатель может вспомнить о том, что iOS внутри является UNIX-подобной системой, а в таких системах обычно доступна особая разновидность файлов — именованные очереди (named pipe), которые позволяют читать и писать в файл, не задействуя при этом дисковые ресурсы.Но AVAssetWriter не умеет работать с таким типом файлов, так как он пишет данные не непрерывным потоком, а иногда возвращаясь назад и дописывая пропущенную информацию, как например при заполнении поля длины атома mdat в MOV-файле.

Для чтения данных из файла, который продолжает записываться в тот же самый момент, самым простым способом будет пытаться в цикле читать новые появившиеся в файле данные, игнорируя признак конца файла, до тех пор, пока запись в выходной файл не будет завершена:[gist id=7152532]Такой подход не очень эффективно расходует ресурсы системы, так как приложение не знает, когда в файле появились новые данные и обращается к файлу постоянно вне зависимости от того, появились они или нет.Этот недостаток можно обойти, задействовав возможности библиотеки GCD для асинхронного ввода/вывода, а именно функции dispatch_source.[gist id=7152637]Полученные таким образом данные будут представлены в том же формате, в котором их пишет iOS, то есть в контейнере MOV или MP4.

Формат видеоконтейнера MOV (также известный как Quicktime File Format) используется в большинстве продуктов Apple.Его спецификация была впервые опубликована в 2001 году и на основе нее ISO стандартизировала более широко используемый сегодня формат MP4.И MOV и MP4 очень похожи и отличаются только небольшим количеством деталей.Видеофайл логически разбит на отдельные иерархически вложенные друг в друга области, называемые атомами.Спецификация MOV описывает порядка пятидесяти различных типов атомов, каждый из которых отвечает за хранение определенной информации.Иногда для правильной интерпретации содержимого одного из атомов необходимо сначала прочитать другой атом.Например, видеопоток, закодированный кодеком H.264, хранится в атоме mdat; для того, чтобы его правильно отобразить в плеере, необходимо сначала прочитать параметры сжатия из атома avcC, временные метки кадров из атома ctts, границы отдельных кадров в mdat из атома stbl и т.д.

MPEG-4 BoxesБольшинство плееров не смогут воспроизводить видеопоток в таком виде, в котором он поступает из незавершенного MOV-файла.Кроме того, для оптимизации передачи видео на сервер и хранения данных может потребоваться преобразовать видеопоток в другой формат или хотя бы распарсить его на отдельные пакеты.Задачу транскодирования видео можно возложить как на серверную часть, так и на клиентскую.В Видеокамере Together мы выбрали второй вариант, так как транскодирование не требует больших вычислительных ресурсов, но позволяет при этом упростить и разгрузить серверную архитектуру.Приложение сразу перекодирует поток в контейнер MPEG TS и нарезает его на сегменты по восемь секунд, которые удобно передавать на сервер простым HTTP POST запросом с телом в формате multipart/form-data.Эти сегменты сразу без дополнительной обработки могут использоваться при построении плейлиста для вещания по протоколу HTTP Live Streaming.Особенности структуры атомов внутри обычного MOV файла делают невозможным применение этого формата в стриминге.Для того, чтобы декодировать любую часть MOV файла, необходимо, чтобы файл был доступен целиком, так как критически важная для декодирования информация содержится в конце файла.Чтобы обойти эту проблему было предложено расширение формата MP4, позволяющее записывать MOV файл, состоящий из множества фрагментов, каждый из которых содержит собственный блок метаинформации о видеопотоке.Для того, чтобы AVAssetWriter начал записывать фрагментированный MOV, достаточно задать значение свойства movieFragmentInterval, означающее длительность фрагмента.Fragmented MP4 Structure

Фрагментированный MP4 (fMP4) используется в таких протоколах стриминга, как Microsoft Smooth Streaming, Adobe HTTP Dynamic Streaming и MPEG-DASH.В Apple HTTP Live Streaming с той же целью используется поток MPEG TS, разбитый на отдельные файлы, называемые сегментами.До выхода iOS 7 был доступен еще один, более сложный способ чтения неполного MOV-файла, который не требовал использования фрагментированного MOV.Можно было распарсить содержимое mdat, выделив отдельные NALU (блок данных кодека H.264) и буферы AAC.Каждый NALU и буфер AAC в выходном файле соответствовал каждому входному сэмпл-буферу и следовал точно в том же порядке, в котором они были записаны.Благодаря этому, можно было легко установить соответствие NALU, кадра и временной метки кадра.Этой информации было достаточно для того, чтобы декодировать видеопоток.В iOS 7 это четкое соответствие перестало работать.Теперь каждому входному сэмпл-буферу могут соответствовать один и более NALU, причем невозможно установить сколько именно для каждого конкретного кадра.

Для транскодирования видеопотока мы использовали наиболее очевидное решение — открытый набор библиотек FFmpeg.Библиотеки FFmpeg позволили нам решить задачу парсинга фрагментированного MOV-файла и перекодирования пакетов в контейнер MPEG TS. FFmpeg позволяет относительно легко распарсить файл представленный в любом формате, переконвертировать его в другой формат и даже передать по сети по любому из поддерживаемых протоколов.Например, для целей лайв-стриминга можно задействовать выходной формат flv и протокол rtmp или протокол rtp.

Для того, чтобы подключить FFmpeg к приложению для iOS, необходимо скомпилировать статические библиотеки FFmpeg в нескольких вариантах под разные архитектуры, добавить их к проекту в Xcode и добавить в опцию Header Search Path настроек сборки путь до заголовочных файлов FFmpeg.Для кросс-компиляции библиотек под iOS необходимо перед сборкой FFmpeg запустить скрипт configure с параметрами, указывающими пути до используемого iOS SDK и набор функциональности, который необходимо включить в сборку.Проще всего скачать один из множества доступных в интернете готовых скриптов сборки (1, 2, 3, 4) и модифицировать его под свои нужды.

Однако, FFmpeg не полностью поддерживает такую схему работы, которую нам пришлось реализовать в Together.Для того, чтобы чтение из пишущегося файла не обрывалось по достижению конца файла, мы написали модуль протокола для FFmpeg, который называется pipelike.Для нарезки сегментов для HTTP LS мы взяли за основу один из ранних вариантов модуля hlsenc из libav и переработали его, исправив найденные ошибки и добавив возможность передавать выходные данные и основные события модуля непосредственно в другие части приложения через колбэки.Можно выделить следующие преимущества и недостатки реализованного решения.Преимущества:

  • Оптимальный расход батареи, не уступающий стандартным приложениям Apple, за счет использования аппаратных средств, доступных на платформе.
  • Максимально возможное использование стандартного iOS SDK, не используются никакие закрытые API, за счет чего решение полностью совместимо с использованием в App Store.
  • Упрощение серверной инфраструктуры за счет транскодирования видео в сегменты HTTP LS на стороне приложения.

Недостатки:

  • Задержка от съемки до воспроизведения порядка 90 секунд.
  • Видеофайл пишется на диск, а это означает, что:
    • Нельзя застримить за один раз видео по длительности больше, чем имеется свободного места на флэш-диске.
    • Во время стриминга расходуется ресурс флэш-диска на количество перезаписываний информации.
    • Занимается место на диске, которое могло бы использоваться под другие нужды.

Из чего складывается задержка:

  1. Передача кадра из камеры в приложение: миллисекунды.
  2. Запись буфера в файл: миллисекунды.
  3. Чтение буфера из файла (включая задержку, обусловленную буферизацией записи в файл на уровне операционной системы и алгоритма кодирования видео): 1-3 секунды.
  4. Завершение чтения фрагмента: до 2 секунд (длительность фрагмента).
  5. Завершение формирования сегмента: до 8 секунд (длительность сегмента).
  6. Отправка сегмента на сервер: 2-10 секунд.
  7. Формирование плейлиста: 2 секунды.
  8. Задержка клиента перед запросом нового плейлиста: 9 секунд (значение Target duration заданное в плейлисте).
  9. Загрузка и буферизация сегментов на клиенте: 27 секунд (три Target duration).

Итого: до 60 секунд.Остальные задержки лайв-стриминга в Together обусловлены особенностями реализации:

  1. Задержка, обусловенная тем, что следущий сегмент начинает считываться только после окончания передачи предыдущего.
  2. Задержка, обусловленная временем прошедшим от начала записи файла до запуска стриминга пользователем (файл всегда стримится с начала).

Большая часть факторов, влияющих на задержку обусловлена использованием протокола HTTP LS. Большинство приложений для лайв-стриминга используют протоколы RTMP или RTP, которые лучше приспособлены для стриминга с минимальной задержкой.Мы провели исследование нескольких других приложений, в которых есть функция лайв-стриминга: Skype, Ustream.Skype имеет минимальную задержку порядка одной секунды, при этом генерирует нагрузку на процессор порядка 50%, из чего можно сделать вывод, что они используют собственный алгоритм сжатия видео и протокол передачи данных.Ustream использует протокол RTMP, имеет задержку не более 10 секунд и минимальную нагрузку на процессор, как при использовании аппаратного кодирования видео.В целом можно отметить, что в результате мы получили приемлемый способ лайв-стриминга, удовлетворяющий требованиям, предъявляемым к приложению Together Video Camera.Другие разработки, использующие похожий подход к лайв-стримингу:

  • Livu — приложение, для стриминга видео с iOS устройства на RTP сервер. Не является полноценным стриминговым сервисом, поэтому вам придется указать собственный видеосервер. На github выложена довольно старая версия кода стриминга этого приложения. Автор приложения Steve McFarlin часто отвечает на Stack Overflow на вопросы, связанные с разработкой видеоприложений.
  • Hardware Video Encoding on iPhone — RTSP Server example — исходники приложения, реализующего лайв-стриминг с iOS.

Смотрите также наши предыдущие статьи по теме разработки видеоприложений под iOS:

  1. Плеер для iOS с видеорекламой в стандарте VAST
  2. Работа с камерой в iOS приложениях
  3. Воспроизведение видео в iOS приложениях