понедельник, 22 мая 2023 г.

[prog.c++] Так вот на счет AVChannelLayout...

...в качестве дополнения к этому посту: Хочется странного: особое отношение C++ного компилятора к структурам, объявленным как extern "C". Попробую вспомнить особенность перехода с FFMPEG 4.4 на FFMPEG 5.1, связанную с AVChannelLayout.

В текущем проекте нужно снимать видео/аудио потоки с камер посредством FFMPEG. В случае с аудио потоками бывает нужно один из них воспроизводить. Так же может потребоваться какие-то из них записывать (если записывать, то вместе с видео).

Приходить аудио может в разных форматах (благо FFMPEG отличается всеядностью), а вот для воспроизведения должен использоваться зафиксированный в коде формат. Для записи также должен использоваться зафиксированный в коде формат, но (ЕМНИП) не такой, как для воспроизведения.

Кроме того, снятые AVFrame протаскиваются через цепочку SObjectizer-овских агентов и i-й агент в цепочке, в принципе, может не иметь всей информации об исходном потоке и его состоянии. Т.е. агент, который напрямую дергает FFMPEG, обнаруживает разрыв и переподключение к камере (соответственно, у него пересоздаются AVCodecContext-ы), а вот последующие агенты могут информацию о переподключениях не получить и не узнать, что новые AVFrame содержат аудио-данные уже в другом формате.

В общем, говоря про код, есть две задачки: a) передать конфигурацию агентам, которые занимаются перекодированием аудио-данных (для воспроизведения или записи) и b) проверять формат аудио-данных в очередном AVFrame чтобы поймать момент внезапной смены формата (если таковая смена вообще происходит).

Для этого была сделана простая структурка, содержащая минимум полей: channel_layout в виде единственного std::int64_t, sample_fmt в виде AVSampleFormat и sample_rate в виде обычного int-а. В общем, тривиальный POD тип с constexpr-конструктором. Экземпляры этого типа используются и для того, чтобы зафиксировать формат аудио для воспроизведения/записи, и для того, чтобы сохранить информацию о параметрах текущего аудио-потока.

Благодаря тому, что описание формата можно было объявлять как constexpr константу, то в нескольких местах кода были сделаны static_assert-ы, для того, чтобы в compile-time проверять, что написанный под определенный аудио-формат код остается актуальным (т.е. если где-то описание формата вдруг поменяли, а код не поправили, то это будет обнаружено компилятором).

Все это хорошо работало потому, что в FFMPEG 4.4 количество и расположение аудио-каналов описывалось, по сути, единственным целым числом -- channel_layout. Иногда этот channel_layout мог быть нулевым (т.е. не заданным явно), тогда значение channel_layout можно было вывести на основании значения nb_channels.

Но вот в FFMPEG 5.1 целочисленное поле channel_layout задеприкейтили (но хотя бы поддерживают), и ввели новый тип данных AVChannelLayout, который является совокупностью из channel_layout+nb_channel из FFMPEG 4.4, но не только. Экземпляр AVChannelLayout теперь может содержать и дополнительную информацию. В том числе и информацию, которая расположена в динамической памяти, т.е. в AVChannelLayout может лежать и вполне себе владеющий указатель.

Поскольку сейчас AVChannelLayout -- это отнюдь не простая структура, то пришлось менять существующий в проекте тип, описывающий формат аудио. Т.е. поле channel_layout типа std::int64_t нужно было заменить на ch_layout типа AVChannelLayout.

И вот здесь как раз начинаются некоторые неудобства. Абсолютно не смертельные, но... Но мимо которых просто так не пройдешь. По крайней мере мне не удалось пройти :)

При этом нужно иметь в виду, что AVChannelLayout -- это value type. Который полностью виден пользователю. И пользователь может создавать AVChannelLayout как на стеке, так и в виде поля в другом объекте/структуре. Т.е. нет функций alloc_av_channel_layout и free_av_channel_layout, которые бы создавали экземпляр в хипе и скрывали бы от пользователя детали инициализации/деинициализации экземпляра AVChannelLayout.


Итак, во-первых, инициализация объекта AVChannelLayout.

FFMPEG рекомендует несколько способов такой инициализации.

Можно создать неинициализированный объект AVChannelLayout на стеке и затем занулить его (например, через memset). А уже после этого вручную установить нужные значения нужных полей. При этом в чисто Си-шном коде такая инициализация занулением делается элементарно, что-то вроде (прошу меня простить если я напутал с Си-шным синтаксисом инициализации):

AVChannelLayout ch_layout{0};

Однако в C++ном коде такой фокус не пройдет, т.к. C++ный компилятор рассматривает это как инициализацию агрегата и ругается на то, что 0 не может просто так приводится к перечислению AVChannelOrder (первое поле в AVChannelLayout имеет именно такой тип). Кроме того, если мне не изменяет склероз, C++ный компилятор должен ругнуться на недостаточное количество инициализаторов для полей.

Так что в C++ нужно сперва создать неинициализированный экземпляр на стеке, а затем вызвать std::memset для зануления. Ну или же нужно выписывать полный агрегирующий инициализатор для AVChannelLayout (что также не есть хорошо, т.к. это может стать потенциальной проблемой, если в последующих версиях FFMPEG что-то добавят в AVChannelLayout или поменяют последовательность полей в нем).

Второй способ -- это применять макросы типа AV_CHANNEL_LAYOUT_STEREO. Т.е. писать что-то вроде:

AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_STEREO();

Однако, эти макросы раскрываются в использование т.н. designated initializers. А в C++17 они не поддерживаются. Формально их в C++17 еще нет, официально завезли в C++20.

Однако, GCC позволяет использовать designated initializers в C++17 коде. Тогда как MSVC++ -- нет.

Так что в рамках C++17 фокус с макросами AV_CHANNEL_LAYOUT_ не проходит в кроссплатформенном коде :(

Третий способ -- это создать неинициализированный экземпляр, а потом проинициализировать его одной из вспомогательных функций. Вроде функции av_channel_layout_default.

В общем, ничего из этого не получается в C++17 (нормально) использовать в constexpr-конструкторе.

Так что от constexpr пришлось отказаться. Как и от нескольких static_assert-ов (что расстроило меня больше всего, ибо люблю проверки в compile-time).


Второе неудобство в том, что поскольку в AVChannelLayout может быть владеющий указатель (голый владеющий указатель, поскольку это чистый Си), то экземпляры AVChannelLayout нельзя просто так копировать и нельзя просто так уничтожать. И для копирования, и для уничтожения нужно использовать специальные функции из FFMPEG.

А это означает, что если мы определим что-то вроде:

struct my_audio_format {
  AVChannelLayout ch_layout_;
  AVSampleFormat fmt_;
  int rate_;
};

То мы на пороге потенциальных багов, т.к. дефолтные оператор копирования и деструктор для my_audio_format не будут обрабатывать владеющий указатель внутри AVChannelLayout :(

Соответственно, при работе с AVChannelLayout в C++ном коде нужно проявлять особую осторожность.


Я же, для того, чтобы не наступать в уже имеющемся коде на такие грабли, просто сделал обертку вокруг AVChannelLayout с нужными мне операторами и деструктором. И далее применял именно эту обертку, а не голый AVChannelLayout. Т.е. у меня my_audio_format выглядела бы так:

struct my_audio_format {
  my_av_channel_layout_wrapper ch_layout_;
  AVSampleFormat fmt_;
  int rate_;
};

На написание такой обертки ушло, ну максимум, полчаса. Однако, по объему она за сотню строк получилась (с комментариями и примерами использования). Так что, боюсь, для неподготовленного человека будет выглядеть страшно и наверняка вызовет вопросы "А нафига такое было нужно" :(

1 комментарий:

DimaGogolev комментирует...

спасибо за развёрнутое описание.
очередной пример того, что обёртки - нужная штука, хотя и нудная.