This post is also available in: Английский
В нашей работе нередко возникает необходимость реализовать отправку видеопотока с iOS-устройства в реальном – или близком к реальному – времени. Самый частый пример — использование iOS-устройства в качестве камеры слежения или создание стриминговых приложений наподобие Periscope. Как правило, при возникновении подобной задачи ставятся дополнительные условия — например, возможность проигрывания потока на другом устройстве (или в другом приложении) без возникновения лишних проблем в браузере или VLC-плеере, малые задержки (видеопоток должен транслироваться практически в режиме реального времени), низкая нагрузка на устройство (возможность длительной работы от батареи), отсутствие необходимости в специализированном медиасервере для обслуживания передачи потока, и т.п.
Подобная задача сама по себе не нова, но до сих пор не имеет однозначного решения. Точнее, она имеет целый спектр возможных решений:
- HTTP Live Streaming (HLS)
- HTTP Dynamic Streaming (HDS)
- MPEG-DASH
- fragmented MP4 (fMP4)
- RTSP
- WebRTC
- и т.д.
Каждый подход обладает со своими плюсами и минусами. Например, в одной из наших прошлых, но не потерявшей актуальность, статье Реализация лайв-стриминга снимаемого видео в приложении для iOS, мы рассмотрели возможность генерации на устройстве готового HLS-потока. Это позволяет в принципе избавиться от необходимости в медиасервере, и сразу загружать контент в CDN (например, Amazon S3 + CloudFront). Однако такой, основанный на HLS, подход изначально имеет недостатки (о которых мы расскажем ниже), поэтому в этот раз мы предлагаем вам два новых варианта – генерацию на устройстве готового FMP4-потока и генерацию RTP-стрима с поддержкой локального RTSP-сервера.
HTTP Live Streaming
Протокол HLS появился в 2009 году и довольно быстро добился неплохой популярности. Этому способствовала его полная поддержка в экосистеме Apple и довольно понятная структура – есть текстовый мастер-файл, содержащий список ссылок на кусочки видеопотока, которые постоянно добавляются (в случае online-трансляции). В HLS также была предусмотрена возможность определять сразу несколько потоков для клиентов с разными требованиями к видео (быстрый канал/медленный канал и т.п.). Однако на практике такая двухступенчатая система и необходимость обновлять «мастер-файл» ограничивает сферу применения HLS. Как показывает практика, HLS слабо приспособлен для трансляций в режиме реального времени из-за следующих проблем:
- Трансляция нарезается на отдельные файлы небольшой длины (рекомендуется несколько секунд), в результате чего отставание трансляции от режима реального времени заложено в самой идее подобной «нарезки».
- При этом стандартная рекомендация по буферизации в видеоплеерах – наличие, как минимум, трех буферизованных сегментов, то есть на уровне плеера задержка из пункта 1 будет утроена;
- Кроме того, чтобы плеер узнал о появлении новых сегментов трансляции, ему необходимо регулярно перезапрашивать мастер-файл, что создает еще один фактор задержки (не обновив мастер-файл, плеер попросту не знает, что воспроизводить дальше).
- Каждый сегмент (chunk) образован как MPEG-TS файл, что означает существенный оверхед к основному медиаконтенту.
Для уменьшения задержек в HLS вполне естественно уменьшать размеры сегментов до минимума (1 секунда). Однако с такими маленькими размерами файлов на первый план выходят естественные нестабильности в работе сети. И в средних, «естественных» условиях добиться плавности воспроизведения практически невозможно — плеер больше оказывается занят перезапросами мастер-файла и очередного сегмента, чем показом видеопотока.
В попытке решить эти проблемы в 2011-2012 годах были предложены аналогичные подходы – MPEG-DASH от MPEG, Smooth Streaming от Microsoft и HDS от Adobe. Но ни одно из них не стало «стандартом де-факто» (хотя из этих трех MPEG-DASH является полноценным стандартом ISO) и на детальном уровне эти решения страдали теми же недостатками, а решения Adobe и Microsoft требовали, к тому же, особой поддержки на стороне сервера. Тут можно посмотреть таблицу сравнения между этими форматами.
Fragmented MP4
Время шло, и в итоге для простой организации трансляций приобрел популярность несколько другой подход – fMP4 (fragmented MP4). Это было небольшое (но существенное) расширение известного и хорошо поддерживаемого формата MP4, который уже тогда воспроизводился практически везде, поэтому широкая поддержка fMP4 тоже не заставила себя ждать.
Вся разница между обычным файлом MP4 и fMP4 заключается в расположении элементов, описывающих видео- и аудиостримы. В обычном MP4-файле подобные элементы расположены в конце, а в fMP4 – в начале. И так как MP4 изначально мог содержать несколько стримов, поделенных на отдельные блоки данных, – то это несложное изменение позволило создать “бесконечный файл” с точки зрения плеера.
Именно этим свойством fMP4 и пользуются при лайв-стриминге. Плеер, прочитав описание видео- или аудиопотока, начинает ждать данные и показывать (проигрывать) их по мере поступления. И если блоки с кадрами генерировать на лету, плеер автоматически будет проигрывать realtime-стрим без дополнительных усилий.
И это действительно работает! Конечно, при реализации генератора fMP4 могут возникнуть некоторые проблемы. Давайте рассмотрим их на конкретном примере – приложении DemoFMP4.
fMP4 Live Streaming on iOS
Это демонстрационное приложение берет кадры с камеры и отправляет в виде fMP4 любому зрителю, подключившемуся к устройству. Для подключения достаточно запросить у устройства виртуальный файл “MP4”, который устройство будет автоматически генерировать по адресу: http://<ip-адрес>:7000/index.mp4.
Для этого приложение запускает небольшой web-сервер на базе GCDWebServer, который слушает порт 7000 и отвечает на запросы на загрузку файла index.mp4.
Здесь есть ряд проблем, с которыми можно столкнуться.
- Во-первых, видеокамера выдает “сырые” кадры, которые нельзя просто так отправить плееру — они должны быть сжаты и собраны в правильном формате. К счастью, начиная с версии iOS 8.0, компания Apple открыла программный доступ к аппаратному сжатию видео, которое умеет генерировать H.264-блоки на лету. Для этого используются семейства функций VTCompressionSessionCreate и VTCompressionSessionEncodeFrame из фреймворка VideoToolbox.
- Аналогично сжимается и аудио функциями семейства AudioConverterNewSpecific / AudioConverterFillComplexBuffer из фреймворка AudoToolbox — в результате чего мы
получаем блоки данных в формате AAC.
- Аналогично сжимается и аудио функциями семейства AudioConverterNewSpecific / AudioConverterFillComplexBuffer из фреймворка AudoToolbox — в результате чего мы
- Во-вторых, камера выдает кадры с довольно высокой скоростью. Чтобы не терять их, мы сохраняем кадры в кольцевых буферах (CBCircularData), из которых блоки кадров по мере заполнения отправляются на сжатие. Эти же кольцевые буфера используются для генерации chunked-ответа, поэтому приложение не хранит в памяти больше наперед заданного числа кадров (иначе для бесконечной трансляции потребовался бы бесконечный объем памяти).
- В третьих, для корректной работы fMP4 необходимо правильно устанавливать начальные данные потока (включая Sps/Pps) в блоке moov MP4-файла. Для этого приложение просматривает H.264-блоки, которые генерирует hardware-кодировщик, находит очередной ключевой кадр и вытаскивает значения Sps/Pps. А при генерации moov-заголовка использует их, чтобы заставить плеер отсчитывать поток от правильного момента во времени. Таким образом, с точки зрения плеера файл всегда показывается “с самого начала”.
- Есть еще одна проблема — формат MP4 имеет свои требования к данным, и не может включать в себя блоки H.264 без правильного оформления. Мы решили эту проблему, подключив замечательную библиотеку Bento4, которая позволяет перепаковывать блоки H.264 в корректные MP4-atoms на лету.
Таким образом у нас получилось приложение, способное транслировать стрим с видеокамеры устройства практически без задержек, в реальном времени, отправляя “бесконечный MP4 файл” любому стандартному HTTP-клиенту. Такой подход дает довольно небольшое отставание: 1-2 секунды. В сочетании с широкой поддержкой fMP4 и простотой организации подобной трансляции (не требуется отдельный сервер) – генерация fMP4 на клиенте становится простым и надежным решением.
True Real-Time Live Streaming
Однако как быть, если нам нужен “настоящий реалтайм”, как в Skype? Для таких ситуаций fMP4, увы, уже не очень подходит. Несмотря на отсутствие мастер-файла (как в HLS) и небольшие размеры блоков, на которые делится видеопоток, эти блоки все еще присутствуют внутри MP4-файла. И пока такой блок не будет собран целиком, клиент никак не сможет получить кадры, что неизбежно создаст небольшую задержку при воспроизведении.
RTSP Live Streaming on iOS
Поэтому для полноценного реалтайма – когда плеер получает кадр практически сразу после его генерации в камере – больше подходит другой формат, изначально разработанный для стриминга. Речь идет о RTSP, который также обладает хорошей поддержкой среди плееров (к примеру, легко проигрывается популярным плеером VLC) и относительно не сложен в реализации. Рассмотрим пример приложения, которое обеспечивает трансляцию по стандарту RTSP – DemoRTSP.
В отличии от HLS и fMP4, которые полагаются для обмена данными на HTTP-протокол, – RTSP изначально использует свой собственный формат поверх “голых сокетов”. Кроме этого, для работы RTSP требуется два канала — один сигнальный, по которому клиент и сервер обмениваются управляющей информацией, а второй “для данных”, по которому сервер отправляет исключительно сжатые данные. Это немного усложняет схему обмена, однако позволяет добиться минимальных задержек – клиент по определению получает данные сразу после того, как сервер отправляет их по сети. В RTSP просто не предусмотрено никаких промежуточных звеньев!
DemoRTSP реализует минимальный набор для такого обмена. При запуске приложение начинает слушать сервисный порт (554) на предмет обращений от клиентов-плееров. При получении обращения, DemoRTSP отправляет в ответ простую строчку с указанием кодека, который будет использован для сжатия видео и аудио (для iOS это стандартная пара H.264/AAC) и номера порта данных, по которому сервер будет отправлять сжатые кадры. После этого клиент-плеер подключается к “порту данных” и проигрывает все, что получает от сервера. В этой схеме сервер никогда ничего не ждет и все сжатые кадры сразу уходят плееру, гарантируя минимальную задержку из возможных.
Надо сказать, что на современных iOS-устройствах сжатие и работа с сетью уже не отнимают много ресурсов. Поэтому, кроме простой трансляции, вполне возможна любого рода дополнительная обработка потока перед отправкой. Например, несложно организовать поверх видео дополнительные оверлеи с информацией, наложить на видео эффект или возможность автоматически менять видео в соответствии с нуждами приложения. Для этого можно использовать как постобработку буфера от камеры, так и более эффективный подход с использованием OpenGL. Остановимся на этом поподробнее.
Live Video Effects on iOS
В нашем демо-приложении DemoRTSP использован весьма простой подход на базе PBVision, но в ваших приложениях вы можете использовать существенно более продвинутые решения на базе фреймворка GPUImage, который позволяет применять к видеопотоку целую цепочку OpenGL эффектов, при этом сведя задержку практически к нулю.
В DemoRTSP можно увидеть пример наложения на стрим простого Blur-эффекта.
Кто сказал, что делать Prisma-like video на лету невозможно!?
Можно пойти еще дальше, и использовать наиболее “быстрый” способ работы с изображениями в iOS на текущий момент — Metal, пришедший на смену OpenGL (в последних версиях iOS). Для примера можно посмотреть в MetalVideoCapture, где показано, как с помощью API-интерфейса CVMetalTextureCache передать результат захвата камеры в Metal Render Pass.
Кроме Metal, iOS последних версий появилась еще несколько интересных возможностей. Одна из них — ReplayKit, позволяющий в несколько строчек организовать трансляцию экрана устройства. Это довольно интересная платформа, однако на текущий момент она не позволяет “вмешиваться” в процесс создания кадра, не позволяет записывать изображение с камеры (только с экрана!), а получение записи ограничено штатными средствами. На текущий момент этот фреймворк не предназначен для полноценного создания управляемых трансляций, и больше похож на “задел на будущее”. Аналогично можно сказать и про другую популярную “новинку”, iOS-поддержку нового языка программирования Swift. К сожалению, он недостаточно хорошо интегрируется с низкоуровневыми возможностями, требуемыми для работы с видео и данными, поэтому в ближайшей перспективе его вряд ли можно будет использовать в подобных задачах (исключая интерфейс).