В LIVEOP мы сосредоточены на предоставлении службам быстрого реагирования наиболее актуальной информации в краткой форме, в то же время не ставя под угрозу удобство взаимодействия с пользователем. Когда мы начали сотрудничать с Zepcam, ведущим мировым поставщиком беспроводных (нательных) систем камер, мы хотели убедиться, что предоставляем опыт, соответствующий нашим высоким стандартам, без ущерба для производительности или эффективности.

Потоки с камеры, размещенные на Zepcam, представлены в нескольких различных форматах, наиболее важно HTTP Live Streaming (HLS), первоклассный гражданин экосистемы iOS со встроенной поддержкой AVFoundation, и RTSP, протокол потоковой передачи в реальном времени. Потоки HLS обычно используются для прямых телетрансляций и выпусков новостей. Он ориентирован на беспрепятственный просмотр для зрителя: пропадание кадров запрещено, воспроизведение кадров вне очереди не допускается, а небольшой буфер предстоящих кадров поддерживается для обеспечения плавного воспроизведения. Ситуации, в которых активируются потоки Zepcam, часто опасны для жизни. Офицеры могут вести прямую трансляцию со своей нательной камеры, пытаясь сдержать бунт, или лестничный двигатель с камерой, установленной наверху, может обеспечивать вид с высоты птичьего полета на большой пожар в здании, включая положение пожарных на земле. Наше определение беспрепятственного взаимодействия с пользователем отличается от определения, предписанного HTTP Live Streaming: в нашем случае важно, чтобы кадры, отображаемые для пользователя, отображались как можно в реальном времени. Они могут прибыть не по порядку, и пара кадров может быть отброшена, если это дает преимущество для реального времени потока. Сложив наши требования, мы пришли к использованию RTSP поверх UDP.

Apple не предоставляет поддержку ни в одной из высокоуровневых платформ для воспроизведения потоков RTSP. MPMoviePlayerController, AVPlayerItem и AVPlayer, все системные классы высокого уровня для воспроизведения видеопотоков, не поддерживают потоки RTSP. К счастью, FFMPEG, швейцарский армейский нож для обработки аудио / видео, оснащен необходимыми инструментами для обработки и декодирования потоков RTSP. FFMPEG существует уже 17 лет в сообществе открытого исходного кода и с тех пор зарекомендовал себя как надежная сила, стоящая за множеством приложений для конечных пользователей, таких как VLC, Google Chrome и Chromium¹.

Настройка FFMPEG

Потоки RTSP, обслуживаемые Zepcam, кодируются кодеком H264. Чтобы предотвратить резкое увеличение двоичного размера нашего окончательного файла приложения iOS (.ipa), мы решили скомпилировать последнюю версию FFMPEG с нуля (v4.0.1), включив только те функции, которые мы ожидаем использовать. Мы используем отличный скрипт сборки, найденный здесь, с парой корректировок:

  • Измените переменную FF_VERSION на 4.0.1
  • Измените DEPLOYMENT_TARGET на цель развертывания вашего приложения iOS.
  • Измените CONFIGURE_FLAGS, чтобы включить битовый код, и отключите все функции, кроме необходимых для нашего потока:
CONFIGURE_FLAGS="--enable-cross-compile --disable-debug --disable-programs --disable-doc --extra-cflags=-fembed-bitcode --extra-cxxflags=-fembed-bitcode --disable-ffmpeg --disable-ffprobe --disable-avdevice --disable-avfilter --disable-encoders --disable-parsers --disable-decoders --disable-protocols --disable-filters  --disable-muxers --disable-bsfs --disable-indevs --disable-outdevs  --disable-demuxers --enable-protocol=file --enable-protocol=tcp --enable-protocol=udp --enable-decoder=mjpeg --enable-decoder=h264 --enable-parser=mjpeg --enable-parser=h264 --enable-parser=aac --enable-demuxer=rtsp --enable-videotoolbox"

Кроме того, требуется небольшое изменение в исходном файле FFMPEG libswresample/arm/audio_convert_neon.S, как описано здесь. Теперь компиляция должна завершиться успешно, в результате получится несколько разных библиотек. Перетащите библиотеки в свой проект Xcode и обязательно свяжите их с целевым объектом приложения (Этапы сборки ›Связывание двоичного файла с библиотеками).

Глобальная настройка, необходимая для воспроизведения видео через FFMPEG, довольно проста. Откройте входной URL-адрес, указывающий на поток RTSP с помощью avformat_open_input, найдите потоки из входа с помощью avformat_find_stream_info, выделите контекст кодека с помощью avcodec_alloc_context3 и avcodec_parameters_to_context и, наконец, откройте кодек с помощью avcodec_open2. Важно реализовать правильную обработку ошибок и очистку памяти для всех этих методов, поскольку все они могут выйти из строя в зависимости от обстоятельств. В нашем приложении мы также решили реализовать обратный вызов прерывания, чтобы на раннем этапе выйти из методов блокировки в определенных ситуациях, например, при отсутствии подключения к Интернету, о чем свидетельствуют SCNetworkReachability API-интерфейсы, или после истечения настраиваемого таймера тайм-аута. В частности, связь с API-интерфейсами достижимости позволяет нам обойти встроенные тайм-ауты FFMPEG и выйти из строя раньше, когда не обнаружено подключение к Интернету.

Декодирование кадров

Структура AVCodecContext предоставляет поле get_format, которое позволяет нам выбрать выходнойAVPixelFormat для видеокадров, доставленных декодером, из списка доступных форматов. Если мы оставим это поле пустым, видеокадры будут отформатированы как AV_PIX_FMT_YUV420P, формат, автоматически определяемый декодером на основе базового потока. Изображения в iOS отформатированы в RGB (A) (AV_PIX_FMT_RGB24), поэтому потребуется дополнительный шаг для преобразования кадров из AV_PIX_FMT_YUV420P в AV_PIX_FMT_RGB24 перед отображением. libswscale предоставляет функцию sws_scale, которая делает именно это, но, к сожалению, она не реализована в графическом процессоре, что означает снижение производительности при выполнении дополнительного шага преобразования из YUV420P в RGB24.

Из списка доступных форматов пикселей, которые мы получаем с помощью функции get_format, есть один, который требует особого внимания: AV_PIX_FMT_VIDEOTOOLBOX. Несмотря на то, что этот формат плохо документирован, этот формат предписывает декодеру передавать входящие кадры на VideoToolbox.framework Apple. Он будет декодировать каждый входящий кадр на GPU, возвращая CVPixelBufferRef, содержащий декодированные данные. Это намного предпочтительнее реализации по умолчанию, которая требует дополнительного преобразования из YUV420 в RGB24 на ЦП. Дескриптор нашей функции, переданный в поле get_format AVCodecContext, теперь выглядит следующим образом:

static enum AVPixelFormat negotiate_pixel_format(struct AVCodecContext *s, const enum AVPixelFormat *fmt) {
    while (*fmt != AV_PIX_FMT_NONE) {
        if (*fmt == AV_PIX_FMT_VIDEOTOOLBOX) {  
            if (s->hwaccel_context == NULL) {
                int result = av_videotoolbox_default_init(s);
                if (result < 0) {
                    return s->pix_fmt;
                }
            }
            return *fmt;
        }
        ++fmt;
    }
    return s->pix_fmt;
}

Мы проверяем доступность формата VideoToolbox, прежде чем пытаться его использовать. Если он недоступен или если инициализация интеграции videotoolbox не удалась, мы возвращаемся к формату, изначально найденному декодером. Обратите внимание, что AV_PIX_FMT_VIDEOTOOLBOX недоступен в симуляторе iOS. В методе teardown нашего класса видеоплеера мы проверяем, не является ли hwaccell_context контекста кодека NULL, и вызываем av_videotoolbox_default_free, если это так.

Отдельные видеокадры принимаются с avcodec_receive_frame. При возврате этой функции выходной параметр AVFrame будет заполнен информацией, кодирующей один видеокадр, в зависимости от используемого формата пикселей. Если формат VideoToolbox был успешно использован, то CVPixelBufferRef, содержащий данные кадра, можно найти в AVFrame.data[3]. Хотя это явно не задокументировано, это очевидно из комментариев, помещенных за определениями других форматов с аппаратным ускорением в pixfmt.h. CVPixelBufferRef не может быть отображен сразу. Сначала мы преобразуем его в CIImage с помощью +[CIImage imageWithCVPixelBuffer:], а затем превращаем CIImage в UIImage с помощью +[UIImage imageWithCIImage:]. FinalUIImage описывает один видеокадр и теперь может отображаться в вашем видеопроигрывателе, например реализован как простой UIImageView.

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

Нам не терпится увидеть, как наша интеграция с Zepcam улучшит рабочий процесс мужчин и женщин, ежедневно работающих над обеспечением безопасности нашего общества.

  1. Https://trac.ffmpeg.org/wiki/Projects