Под лайв-стримингом мы понимаем функцию приложения, которая позволяет передавать видео по сети одновременно со съемкой, не дожидаясь полного окончания записи видеофайла.В этой статье мы хотим рассказать о том, как мы подошли к ее решению в ходе разработки Видеокамеры Together для iOS.
На первый взгляд в лайв-стриминге не должно быть ничего сложного.Существует множество широко известных приложений и сервисов для самых разных платформ, которые используют эту функцию.Для некоторых из них она является основной: 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 так и сгенерировать программно.
Для отправки видео по сети идеальным вариантом было бы пропустить этап сохранения данных на диск, а вместо этого сразу получать сжатые данные непосредственно в память приложения.Но, к сожалению, в SDK отсутствует такая возможность, поэтому единственное, что остается — это считывать сжатые данные из файла, в который пишет система.
Для чтения данных из файла, который продолжает записываться в тот же самый момент, самым простым способом будет пытаться в цикле читать новые появившиеся в файле данные, игнорируя признак конца файла, до тех пор, пока запись в выходной файл не будет завершена:[gist id=7152532]Такой подход не очень эффективно расходует ресурсы системы, так как приложение не знает, когда в файле появились новые данные и обращается к файлу постоянно вне зависимости от того, появились они или нет.Этот недостаток можно обойти, задействовав возможности библиотеки GCD для асинхронного ввода/вывода, а именно функции dispatch_source.[gist id=7152637]Полученные таким образом данные будут представлены в том же формате, в котором их пишет iOS, то есть в контейнере MOV или MP4.
Большинство плееров не смогут воспроизводить видеопоток в таком виде, в котором он поступает из незавершенного MOV-файла.Кроме того, для оптимизации передачи видео на сервер и хранения данных может потребоваться преобразовать видеопоток в другой формат или хотя бы распарсить его на отдельные пакеты.Задачу транскодирования видео можно возложить как на серверную часть, так и на клиентскую.В Видеокамере Together мы выбрали второй вариант, так как транскодирование не требует больших вычислительных ресурсов, но позволяет при этом упростить и разгрузить серверную архитектуру.Приложение сразу перекодирует поток в контейнер MPEG TS и нарезает его на сегменты по восемь секунд, которые удобно передавать на сервер простым HTTP POST запросом с телом в формате multipart/form-data.Эти сегменты сразу без дополнительной обработки могут использоваться при построении плейлиста для вещания по протоколу HTTP Live Streaming.Особенности структуры атомов внутри обычного MOV файла делают невозможным применение этого формата в стриминге.Для того, чтобы декодировать любую часть MOV файла, необходимо, чтобы файл был доступен целиком, так как критически важная для декодирования информация содержится в конце файла.Чтобы обойти эту проблему было предложено расширение формата MP4, позволяющее записывать MOV файл, состоящий из множества фрагментов, каждый из которых содержит собственный блок метаинформации о видеопотоке.Для того, чтобы AVAssetWriter начал записывать фрагментированный MOV, достаточно задать значение свойства movieFragmentInterval, означающее длительность фрагмента.
Для транскодирования видеопотока мы использовали наиболее очевидное решение — открытый набор библиотек FFmpeg.Библиотеки FFmpeg позволили нам решить задачу парсинга фрагментированного MOV-файла и перекодирования пакетов в контейнер MPEG TS. FFmpeg позволяет относительно легко распарсить файл представленный в любом формате, переконвертировать его в другой формат и даже передать по сети по любому из поддерживаемых протоколов.Например, для целей лайв-стриминга можно задействовать выходной формат flv и протокол rtmp или протокол rtp.
Однако, FFmpeg не полностью поддерживает такую схему работы, которую нам пришлось реализовать в Together.Для того, чтобы чтение из пишущегося файла не обрывалось по достижению конца файла, мы написали модуль протокола для FFmpeg, который называется pipelike.Для нарезки сегментов для HTTP LS мы взяли за основу один из ранних вариантов модуля hlsenc из libav и переработали его, исправив найденные ошибки и добавив возможность передавать выходные данные и основные события модуля непосредственно в другие части приложения через колбэки.Можно выделить следующие преимущества и недостатки реализованного решения.Преимущества:
- Оптимальный расход батареи, не уступающий стандартным приложениям Apple, за счет использования аппаратных средств, доступных на платформе.
- Максимально возможное использование стандартного iOS SDK, не используются никакие закрытые API, за счет чего решение полностью совместимо с использованием в App Store.
- Упрощение серверной инфраструктуры за счет транскодирования видео в сегменты HTTP LS на стороне приложения.
Недостатки:
- Задержка от съемки до воспроизведения порядка 90 секунд.
- Видеофайл пишется на диск, а это означает, что:
- Нельзя застримить за один раз видео по длительности больше, чем имеется свободного места на флэш-диске.
- Во время стриминга расходуется ресурс флэш-диска на количество перезаписываний информации.
- Занимается место на диске, которое могло бы использоваться под другие нужды.
Из чего складывается задержка:
- Передача кадра из камеры в приложение: миллисекунды.
- Запись буфера в файл: миллисекунды.
- Чтение буфера из файла (включая задержку, обусловленную буферизацией записи в файл на уровне операционной системы и алгоритма кодирования видео): 1-3 секунды.
- Завершение чтения фрагмента: до 2 секунд (длительность фрагмента).
- Завершение формирования сегмента: до 8 секунд (длительность сегмента).
- Отправка сегмента на сервер: 2-10 секунд.
- Формирование плейлиста: 2 секунды.
- Задержка клиента перед запросом нового плейлиста: 9 секунд (значение Target duration заданное в плейлисте).
- Загрузка и буферизация сегментов на клиенте: 27 секунд (три Target duration).
Итого: до 60 секунд.Остальные задержки лайв-стриминга в Together обусловлены особенностями реализации:
- Задержка, обусловенная тем, что следущий сегмент начинает считываться только после окончания передачи предыдущего.
- Задержка, обусловленная временем прошедшим от начала записи файла до запуска стриминга пользователем (файл всегда стримится с начала).
Большая часть факторов, влияющих на задержку обусловлена использованием протокола 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: