Опыт использования NewTek NDI® C SDK

NDI® — это royalty free стандарт, разработанный компанией NewTek, предназначенный для обмена видео в локальной сети. NDI® позволяет нескольким видеосистемам находить друг друга и взаимодействовать в пределах локальной сети, кодировать, передавать и принимать множество аудио и видео потоков с низкой задержкой в режиме реального времени. Может работать в двунаправленном режиме со множеством видеопотоков через общее соединение. Алгоритмы кодирования работают независимо от частоты кадров, поддерживается 4К видео, 16 и более каналов аудио с плавающей точкой. Стандарт включает средства реализации прав доступа к видео, группировку, отправку и получение метаданных и команд.

В этой заметке я расскажу о внедрении этой технологии для отправки аудио и видео потока в существующий пайплайн, а так же о некоторых неочевидных на первый взгляд подводных камнях.

Технология NDI®

Технология NewTek NDI® предоставляет возможность на основе существующей инфраструктуры Gigabit Ethernet создавать студии видеомонтажа в реальном времени профессионального уровня. Используя программное обеспечение с поддержкой ввода-вывода NDI потоков, применяя дополнительные видеоэффекты, микширование видео и аудио, используя режимы Picture in Picture, Lower Thirds, эффекты бегущей строки, существует возможность создавать и доставлять уникальный контент своим зрителям в режиме реального времени. Все перечисленные видеоэффекты можно воспроизвести используя пайплайн — настраиваемый конвейер покадровых графических преобразований, реализованный внутри мультимедиа-приложения. Источниками видеопотоков внутри локальной сети могут являться другие компьютеры, широко доступные платы захвата и конвертации SDI, HDMI в NDI, IP-камеры с поддержкой такой функциональности, Skype во время звонка, и даже обычные смартфоны (захват экрана либо использование основной или фронтальной камеры).

NDI-logo

Вот некоторые примеры использования:

  • запись и трансляция конференций, во время которой можно осуществить real-time монтаж захваченной презентации с экрана, нескольких видеокамер в зале, стриминг в интернет и запись на носитель;
  • проведение онлайн вебинаров со множеством участников и real-time монтажом;
  • захват экрана/окна с игрой, объединение с изображением видеокамеры, добавление виджетов комментариев, уведомлений и пожертвований с последующей отправкой на популярные стриминговые сервисы (twitch, youtube и т.п.).

Скачать NDI® SDK можно бесплатно по адресу https://www.ndi.tv/sdk/, требуется регистрация.

Получение данных

Создание Find Instance

Для получения данных из какого-либо источника NDI, этот источник сначала необходимо обнаружить. Для этого нужно создать find instance типа NDIlib_find_instance_t, для чего предлагается функция NDIlib_find_create_v2:

NDIlib_find_instance_t NDIlib_find_create_v2(const NDIlib_find_create_t* p_create_settings);

В функцию создания можно передать указатель на структуру NDIlib_find_create_t, в которой указать следующие поля:

  • bool show_local_sources — необходимо ли отражать в списке доступных источников локальные;
  • const char* p_groups — список групп источников для поиска;
  • const char* p_extra_ips — дополнительный список IP-адресов для поиска источников, разделенных запятой.

Периодическим вызовом функции (допустим 1 раз в секунду) NDIlib_find_get_current_sources на созданном инстансе NDIlib_find_instance_t получим указатель на массив источников типа NDIlib_source_t:

const NDIlib_source_t* NDIlib_find_get_current_sources(
    NDIlib_find_instance_t p_instance, 
    uint32_t* p_no_sources);

Источник NDI

Объект источника типа NDIlib_source_t содержит в себе следующие поля:

  • const char* p_ndi_name — уникальное название источника в локальной сети вида MACHINE_NAME (NDI_SOURCE_NAME);
  • const char* p_url_address — адрес источника, хранится для внутреннего использования.

Выбрав необходимый источник по его названию, существует возможность создания receiver instance типа NDIlib_recv_instance_t функцией NDIlib_recv_create_v3:

NDIlib_recv_instance_t NDIlib_recv_create_v3(
    const NDIlib_recv_create_v3_t* p_create_settings); 

где p_create_settings указатель на структуру NDIlib_recv_create_v3_t, содержащую следующие поля:

  • NDIlib_source_t source_to_connect_to — полученный ранее объект источника;
  • NDIlib_recv_color_format_e color_format — предпочитаемый формат хранения цвета;
  • NDIlib_recv_bandwidth_e bandwidth — настройка ширины канала.

Цветовой формат

enum NDIlib_recv_color_format_e позволяет определить предпочитаемый цветовой формат для изображений, как содержащих, так и не содержащих альфа-канал. Это достигается выбором пары форматов: например, NDIlib_recv_color_format_UYVY_BGRA — для не содержащих альфа-канала изображений будет использован формат UYVY, а для содержащих — BGRA. NDI SDK предлагает возможность автоматического выбора формата для достижения лучшей производительности/лучшего качества:

  • NDIlib_recv_color_format_fastest;
  • NDIlib_recv_color_format_best.

Соответственно, для внедрения поступающих кадров в свой пайплайн необходимо предусмотреть конвертацию такого формата во внутренний.

Настройка ширины канала

Для настройки ширины канала предусмотрен enum NDIlib_recv_bandwidth_e со следующими вариантами:

  • NDIlib_recv_bandwidth_metadata_only — только получение метаданных от источника;
  • NDIlib_recv_bandwidth_audio_only — получение метаданных и аудио кадров;
  • NDIlib_recv_bandwidth_lowest — получение метаданных, аудио кадров и видео кадров в низком качестве;
  • NDIlib_recv_bandwidth_highest — получение метаданных, аудио кадров и видео кадров в исходном разрешении.

Объект синхронизации

Чтобы получить аудио и видео кадры, синхронизированные по времени, необходимо создать объект синхронизации типа NDIlib_framesync_instance_t. Для этого достаточно вызвать функцию NDIlib_framesync_create, передав туда параметр указатель на NDIlib_recv_instance_t:

NDIlib_framesync_instance_t NDIlib_framesync_create(NDIlib_recv_instance_t p_receiver);

Получение аудио и видео кадров возможно с помощью следующих функций:

void NDIlib_framesync_capture_audio(NDIlib_framesync_instance_t p_instance,
                                    NDIlib_audio_frame_v2_t* p_audio_data,
                                    int sample_rate, int no_channels, int no_samples);
void NDIlib_framesync_capture_video(NDIlib_framesync_instance_t p_instance,
                                    NDIlib_audio_frame_v2_t* p_video_data);

Соответственно нужно создать объекты типов NDIlib_audio_frame_v2_t и NDIlib_audio_frame_v2_t, которые просто создаются на стеке и, в дальнейшем, освобождаются. После копирования кадров в пайплайн NDI кадры необходимо освободить:

void NDIlib_framesync_free_audio(NDIlib_framesync_instance_t p_instance,
                                 NDIlib_audio_frame_v2_t* p_audio_data);
void NDIlib_framesync_free_video(NDIlib_framesync_instance_t p_instance,
                                 NDIlib_video_frame_v2_t* p_video_data);

Окончание работы

Для окончания работы с получением аудио и видео кадров достаточно освободить объект синхронизации и receiver instance вызовом следующих функций:

void NDIlib_framesync_destroy(NDIlib_framesync_instance_t p_instance);
void NDIlib_recv_destroy(NDIlib_recv_instance_t p_instance);

Отправка данных

Создание Sender Instance

Для отправки аудио/видео кадров создателями NDI® SDK предлагается использование объекта типа NDIlib_send_instance_t, который создается с помощью метода NDIlib_send_create:

NDIlib_send_instance_t NDIlib_send_create(const NDIlib_send_create_t* p_create_settings);

Создание объекта можно параметризировать, передав указатель на структуру типа NDIlib_send_create_t. В ней можно установить параметры отправителя:

  • const char* p_ndi_name — имя NDI® источника;
  • const char* p_groups — частью какой группы этот источник является;
  • bool clock_video, clock_audio — параметры, указывающие на способ синхронизации аудио/видео в случае отправки из разных потоков приложения.

Сразу после создания происходит отправка метаданных источника с использованием функции NDIlib_send_add_connection_metadata:

void NDIlib_send_add_connection_metadata(NDIlib_send_instance_t p_instance, 
                                         const NDIlib_metadata_frame_t* p_metadata);

Эта функция позволяет указать производителя, версию, модель и название оборудования источника. Для ее использования необходимо заполнить структуру NDIlib_metadata_frame_t, в которой в поле p_data записать указатель на строку с заполненной XML-структурой:

<ndi_product long_name="Stream Name"
             short_name="Short Stream Name"
             manufacturer="Manufacturer"
             version="1.0.0"
             session="default"
             model_name="device model"
             serial="device serial"/>

Отправка видео кадров

Для отправки видео кадров NDI® SDK предлагает использование функций с ранее созданным sender instance:

void NDIlib_send_send_video_v2(NDIlib_send_instance_t p_instance, 
                              const NDIlib_video_frame_v2_t* p_video_data);
void NDIlib_send_send_video_async_v2(NDIlib_send_instance_t p_instance, 
                                     const NDIlib_video_frame_v2_t* p_video_data);

В документации к NDI® SDK рекомендуется использовать NDIlib_send_send_video_async_v2, т.к. происходит мгновенный возврат из функции, а обработка и отправка кадра происходит асинхронно. Для отправки необходимо заполнить структуру типа NDIlib_video_frame_v2_t:

  • int xres, yres — разрешение кадра;
  • NDIlib_FourCC_video_type_e FourCC — тип кадра;
  • int frame_rate_N и frame_rate_D — числитель и знаменатель количества кадров в секунду;
  • uint8_t* p_data — указатель на данные;
  • int64_t timecode — таймкод в 100 наносекундных интервалах;
  • int line_stride_in_bytes — количество байт в одной строке кадра (страйд).

FourCC рекомендуется NDIlib_FourCC_type_UYVY либо NDIlib_FourCC_video_type_UYVA (в случае наличия альфа-канала), однако, если в пайплайне не предусмотрена возможность произвести color conversion аппаратно, то NDI® может (и рекомендует) произвести преобразование самостоятельно с использованием векторных инструкций современных процессоров.

Также, при использовании этой функции, необходимо проследить, чтобы данные, переданные через указатель p_data не были переиспользованы либо удалены до следующего "синхронизирующего события". Такими событиями могут являться вызов следующих функций:

  • NDIlib_send_send_video;
  • NDIlib_send_send_video_async с другим кадром для отправки;
  • NDIlib_send_send_video с p_video_data=NULL;
  • NDIlib_send_destroy.

Отправка аудио кадров

Для отправки аудио кадров NDI® SDK предлагает использование одной из множества функций из заголовочного файла Processing.NDI.Send.h:

void NDIlib_send_send_audio_v2(NDIlib_send_instance_t p_instance, 
                               const NDIlib_audio_frame_v2_t* p_audio_data);
void NDIlib_send_send_audio_v3(NDIlib_send_instance_t p_instance, 
                               const NDIlib_audio_frame_v3_t* p_audio_data);

А также Processing.NDI.utilities.h:

void NDIlib_util_send_send_audio_interleaved_16s(
    NDIlib_send_instance_t p_instance,
    const NDIlib_audio_frame_interleaved_16s_t* p_audio_data);
void NDIlib_util_send_send_audio_interleaved_32s(
    NDIlib_send_instance_t p_instance,
    const NDIlib_audio_frame_interleaved_32s_t* p_audio_data);
void NDIlib_util_send_send_audio_interleaved_32f(
    NDIlib_send_instance_t p_instance, 
    const NDIlib_audio_frame_interleaved_32f_t* p_audio_data);

Для отправки использована функция NDIlib_util_send_send_audio_interleaved_32f, т.к. именно такой формат аудио используется в нашем пайплайне. Соответственно для ее использования заполняется структура типа NDIlib_audio_frame_interleaved_32f_t:

  • int sample_rate — частота дискретизации (48000 или 44100 сэмплов в секунду);
  • int no_channels — количество каналов (1 — моно, 2 — стерео и т.д.);
  • int no_samples — количество сэмплов в буфере данных;
  • int64_t timecode — временной код начала аудио кадра;
  • float* p_data — указатель на буфер аудио кадра.

Параметр clock_audio работает неочевидным из документации способом: если он установлен в true, то при отправке кадра аудиоданных внутри функции отправки происходит задержка на всю длительность этого кадра, что может сыграть злую шутку при использовании в приложении этого же кадра в других целях. Допустим, что в приложении есть поток получения аудиоданных, и внутри этого потока вызывается метод renderAudioFrame:

void renderAudioFrame(float * data, size_t sampleCount) {
    for (auto &output : _outputs) {
        output->renderAudioFrame(data, sampleCount);
    }
}

В таком случае, при использовании параметра clock_audio == true, другие получатели аудиоданных будут получать их с недопустимой задержкой. Выходом из данной ситуации может являться отказ от использования этого параметра, либо реализация многопоточной очереди.

Таким образом для синхронизации аудио и видео кадров было использовано явное указание временных кодов соответствующих кадров.

Окончание работы

Для прекращения отправки кадров в NDI® необходимо вызвать функцию NDIlib_send_send_video_v2 c p_video_data=NULL для инициации синхронизирующего события и последующим удалением sender instance вызовом функции NDIlib_send_destroy.

Заключение

Использование NDI® SDK позволяет относительно простым способом добавить возможность отправки и получения видеопотока через локальную сеть в свой пайплайн, что позволит встроить мультимедиа-приложение в этот технологический стек и обогатить его дополнительной функциональностью.