Как добавить кодек в FFmpeg


    FFmpeg — это грандиозный Open Source проект, своего рода мультимедийная энциклопедия. С помощью FFmpeg можно решить огромное число задач компьютерного мультимедиа. Но все-таки иногда возникает необходимость в расширении FFmpeg. Стандартный способ — это внесение изменений в код проекта с последующей компиляцией новой версии. В статье подробно рассмотрено, как добавить новый кодек. Также рассмотрены некоторые возможности для подключения к FFmpeg внешних функций. Если нет необходимости добавлять кодек, то статья может оказаться полезной для лучшего понимания архитектуры кодеков FFmpeg и их настройки. Предполагается, что читатель знаком с архитектурой FFmpeg, процессом компиляции FFmpeg, а также имеет опыт программирования с использованием FFmpeg API. Описание актуально для FFmpeg 4.2 «Ada», август 2019.



    Оглавление



    Введение


    Кодек (codec, происходит от объединения терминов COder и DECoder) является весьма распространенным термином и, как в таких случаях часто бывает, его значение несколько меняется в зависимости от контекста. Основное значение — это программное или аппаратное средство для сжатия/разжатия (compression/decompression) медиаданных. Вместо терминов сжатие/разжатие часто используют термины кодирование/декодирование (encoding/decoding). Но в ряде случаев под кодеком понимают скорее просто формат сжатия (говорят еще формат кодека), безотносительно к средствам, используемым для сжатия/разжатия. Посмотрим как термин кодек используется в FFmpeg.



    1. Идентификация кодеков


    FFmpeg кодеки собраны в библиотеке libavcodec.



    1.1. Идентификатор кодека


    В файле libavcodec/avcodec.h определено перечисление enum AVCodecID. Каждый элемент этого перечисления как раз и идентифицирует формат сжатия. Элементы этого перечисления должны иметь вид AV_CODEC_ID_XXX, где XXX уникальное имя идентификатора кодека в верхнем регистре. Вот примеры идентификаторов кодека: AV_CODEC_ID_H264, AV_CODEC_ID_AAC. Для более подробного описания идентификатора кодека служит структура AVCodecDescriptor (объявлена в libavcodec/avcodec.h, приводится в сокращенном виде):


    typedef struct AVCodecDescriptor {
        enum AVCodecID   id;
        enum AVMediaType type;
        const char      *name;
        const char      *long_name;
    // ...
    } AVCodecDescriptor;

    Ключевым членом этой структуры является id, остальные члены как раз и дают дополнительную информацию об идентификаторе кодека. Каждый идентификатор кодека однозначно связан с типом медиаданных (член type) и имеет уникальное имя (член name), записанное в нижнем регистре. В файле libavcodec/codec_desc.c определен массив типа AVCodecDescriptor. Для каждого идентификатора кодека имеется соответствующий элемент массива. Элементы этого массива должны быть упорядочены по значениям id, так как для поиска элементов используется двоичный поиск. Для получения информации об идентификаторе кодека можно использовать функции:


    const AVCodecDescriptor*
    avcodec_descriptor_get(enum AVCodecID id);
    const AVCodecDescriptor*
    avcodec_descriptor_get_by_name(const char *name);
    enum AVMediaType
    avcodec_get_type(enum AVCodecID codec_id);
    const char*
    avcodec_get_name(enum AVCodecID id);


    1.2. Кодек


    Собственно кодек — набор средств, необходимых для выполнения кодирования/декодирования медиаданных, объединяет структура AVCodec (объявлена в libavcodec/avcodec.h). Вот ее сокращенная версия, более полная будет рассматриваться ниже.


    typedef struct AVCodec {
        const char *name;
        const char *long_name;
        enum AVMediaType type;
        enum AVCodecID   id;
    // ...
    } AVCodec;

    Важнейший член этой структуры — это id, идентификатор кодека, также есть член определяющий тип медиаданных (type), но его значение должно совпадать со значением такого же члена из AVCodecDescriptor. Кодеки подразделяются на две категории — кодеры (encoders), которые осуществляют сжатие или кодирование медиаданных, и декодеры (decoders), которые осуществляют обратную операцию — разжатие или декодирование. (В русских текстах иногда вместо термина кодер используют кальку с английского — энкодер.) Специального члена в AVCodec, определяющего категорию кодека, нет (правда категорию можно определить косвенно, с помощью функций av_codec_is_encoder() и av_codec_is_decoder(), эта категория определяется при регистрации. Как это делается будет показано ниже. Несколько кодеков могут иметь один и тот же идентификатор кодека. Если у них одна и та же категория, они должны различаться по именам (член name). Кодер и декодер, имеющие один и тот же идентификатор кодека, могут иметь одно то же имя, которое к тому же может совпадать с именем идентификатора кодека (но эти совпадения не обязательны). Такая ситуация может привести к некоторой путанице, но тут ничего не поделать, необходимо четко понимать к какой сущности относится имя. В пределах одной категории имя кодека должно быть уникально. Для поиска зарегистрированных кодеков имеются функции:


    AVCodec* avcodec_find_encoder_by_name(const char *name);
    AVCodec* avcodec_find_decoder_by_name(const char *name);
    AVCodec* avcodec_find_encoder(enum AVCodecID id);
    AVCodec* avcodec_find_decoder(enum AVCodecID id);

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


    Список всех зарегистрированных кодеков можно запросить командой


    ffmpeg -codecs >codecs.txt


    После выполнения команды, файл codecs.txt будет содержать этот список. Каждый идентификатор кодека будет представлен отдельной записью (строкой). Вот, например, запись для идентификатора кодека AV_CODEC_ID_H264:


    DEV.LS
    h264
    H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
    (decoders: h264 h264_qsv h264_cuvid)
    (encoders: libx264 libx264rgb h264_amf h264_nvenc h264_qsv nvenc nvenc_h264)


    В начале записи находятся специальные символы, определяющие имеющиеся общие возможности для данного идентификатора кодека: D — зарегистрированы декодеры, E — зарегистрированы кодеры, V — используется для видео, L — имеется возможность сжатия с потерями, S — имеется возможность сжатия без потерь. Далее идет имя идентификатора кодека (h264), после него длинное имя идентификатора кодека (H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10), и после этого список имен зарегистрированных декодеров и кодеров.



    2. Добавление нового кодека в FFmpeg


    Процедуру добавления нового кодека в FFmpeg рассмотрим на примере аудиокодека, который назовем FROX.


    Шаг 1. Добавить новый элемент в перечисление enum AVCodecID.


    Это перечисление находится в файле libavcodec/avcodec.h. При добавлении надо соблюдать правила:


    1. Значение элемента не должно совпадать со значениями существующих элементов перечисления;
    2. Не менять значения существующих элементов перечисления;
    3. Размещать новое значение в группе сходных кодеков.

    В соответствии с шаблоном, идентификатор этого элемента должен быть AV_CODEC_ID_FROX. Разместим его перед AV_CODEC_ID_PCM_S64LE и дадим значение 0x10700.


    Шаг 2. Добавить элемент в массив codec_descriptors (файл libavcodec/codec_desc.c).


    static const AVCodecDescriptor codec_descriptors[] = {
    // ...
        {
            .id = AV_CODEC_ID_FROX,
            .type = AVMEDIA_TYPE_AUDIO,
            .name = "frox",
            .long_name = NULL_IF_CONFIG_SMALL("FROX audio"),
            .props = AV_CODEC_PROP_LOSSLESS,
        },
    // ...
    };

    Добавить элемент надо в «правильное» место, не должна нарушаться монотонность элементов массива по значению id.


    Шаг 3. Определить экземпляры AVCodec отдельно для кодера и декодера.


    Для этого предварительно надо определить структуру для контекста кодека и несколько функций, которые и будут выполнять фактическое кодирование/декодирование и некоторые другие необходимые операции. В данном разделе эти определения будут сделаны предельно схематично, более детальное описание будет сделано дальше. Код разместим в файле libavcodec/frox.c.


    #include "avcodec.h"
    
    // context
    
    typedef struct FroxContext {
    // ...
    } FroxContext;
    
    // decoder
    
    static int frox_decode_init(AVCodecContext *codec_ctx)
    {
        return -1;
    }
    
    static int frox_decode_close(AVCodecContext *codec_ctx)
    {
        return -1;
    }
    
    static int frox_decode(AVCodecContext *codec_ctx,
    void* outdata, int *outdata_size, AVPacket *pkt)
    {
        return -1;
    }
    
    AVCodec ff_frox_decoder = {
        .name = "frox_dec",
        .long_name = NULL_IF_CONFIG_SMALL("FROX audio decoder"),
        .type = AVMEDIA_TYPE_AUDIO,
        .id = AV_CODEC_ID_FROX,
        .priv_data_size = sizeof(FroxContext),
        .init = frox_decode_init,
        .close = frox_decode_close,
        .decode = frox_decode,
        .capabilities = AV_CODEC_CAP_LOSSLESS,
        .sample_fmts = (const enum AVSampleFormat[])
            {AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE},
        .channel_layouts = (const int64_t[])
            {AV_CH_LAYOUT_MONO, 0 },
    };
    
    // encoder
    
    static int frox_encode_init(AVCodecContext *codec_ctx)
    {
        return -1;
    }
    
    static int frox_encode_close(AVCodecContext *codec_ctx)
    {
        return -1;
    }
    
    static int frox_encode(AVCodecContext *codec_ctx,
    AVPacket *pkt, const AVFrame *frame, int *got_pkt_ptr)
    {
        return -1;
    }
    
    AVCodec ff_frox_encoder = {
        .name = "frox_enc",
        .long_name = NULL_IF_CONFIG_SMALL("FROX audio encoder"),
        .type = AVMEDIA_TYPE_AUDIO,
        .id = AV_CODEC_ID_FROX,
        .priv_data_size = sizeof(FroxContext),
        .init = frox_encode_init,
        .close = frox_encode_close,
        .encode2 = frox_encode,
        .sample_fmts = (const enum AVSampleFormat[])
            {AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE},
        .channel_layouts = (const int64_t[])
            {AV_CH_LAYOUT_MONO, 0 },
    };

    Для простоты в этом примере кодер и декодер имеют один и тот же один контекст — FroxContext, но чаще всего кодер и декодер имеют разные контексты. Также обратим внимание на то, что имена экземпляров AVCodec должны следовать специальному шаблону.


    Шаг 4. Добавить экземпляры AVCodec в список регистрации.


    Переходим в файл libavcodec/allcodecs.c. В начале этого файла находятся список объявлений всех регистрируемых кодеков. Добавляем в этот список наши кодеки:


    extern AVCodec ff_frox_decoder;
    extern AVCodec ff_frox_encoder;

    В процессе выполнения скрипт configure находит все такие объявления и генерирует файл libavcodec/codec_list.c, который содержит массив указателей на кодеки, объявленные в libavcodec/allcodecs.c. После выполнения скрипта в файле libavcodec/codec_list.c мы увидим:


    static const AVCodec * const codec_list[] = {
    // ...
        &ff_frox_encoder,
    // ...
        &ff_frox_decoder,
    // ...
        NULL };

    Также в процессе выполнения скрипт configure генерирует файл config.h, в котором мы найдем объявления


    #define CONFIG_FROX_DECODER 1
    #define CONFIG_FROX_ENCODER 1

    Шаг 5. Отредактировать libavcodec/Makefile


    Открываем libavcodec/Makefile. Находим раздел # decoders/encoders, и добавляем туда


    OBJS-$(CONFIG_FROX_DECODER) += frox.o
    OBJS-$(CONFIG_FROX_ENCODER) += frox.o

    Шаг 6. Отредактировать код мультиплексора и демультиплексора.


    Мультиплексор (muxer) и демультиплексор (demuxer) должны «знать» новый кодек. При записи необходимо записать идентифицирующую информацию для этого кодека, при чтении определить идентификатор кодека по идентифицирующей информации. Вот что нужно сделать для формата matroska (файлы *.mkv).


    1. В файле libavformat/matroska.c в массив ff_mkv_codec_tags добавить элемент для нового кодека:


    const CodecTags ff_mkv_codec_tags[] = {
    // ...
        {"A_FROX", AV_CODEC_ID_FROX},
    // ...
    };

    Строка "A_FROX" и будет записываться мультиплексором в файл в качестве идентифицирующей информации. В данном массиве она связывается с идентификатором кодека, поэтому демультиплексор при чтении сможет легко его определить. Демультиплексор записывает идентификатор кодека в член codec_id структуры AVCodecParameters. Указатель на эту структуру является членом структуры AVStream.


    2. В файле libavformat/matroskaenc.c в массив additional_audio_tags добавить элемент:


    static const AVCodecTag additional_audio_tags[] = {
    // ...
        { AV_CODEC_ID_FROX, 0XFFFFFFFF },
    // ...
    };

    Итак все готово. Сначала запускаем скрипт configure. После этого надо убедится, что описанные выше изменения в файлах libavcodec/codec_list.c и config.h сделаны. После чего можно запускать компиляцию:


    make clean
    make


    Если компиляция прошла без ошибок, появляется исполняемый файл ffmpeg (или ffmpeg.exe, если целевой ОС является Windows). Выполняем команду


    ./ffmpeg -codecs >codecs.txt


    и убеждаемся, что FFmpeg «видит» наши новые кодеки, в файле codecs.txt находим запись


    DEA..S frox FROX audio (decoders: frox_dec) (encoders: frox_enc)



    3. Подробное описание контекста и необходимых функций


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



    3.1. Контекст кодека


    Контекст кодека может поддерживать установку опций. Для кодеров эта поддержка используется достаточно часто, для декодеров реже. Структура, поддерживающая установку опций, должна в качестве первого члена иметь указатель на структуру AVClass и далее сами опции.


    #include "libavutil/opt.h"
    
    typedef struct FroxContext {
        const AVClass *av_class;
        int      frox_int;
        char    *frox_str;
        uint8_t *frox_bin;
        int      bin_size;
    } FroxContext;

    Далее надо определить массив типа AVOption, каждый элемент которого и описывает конкретную опцию.


    static const AVOption frox_options[] = {
      { "frox_int",
        "This is a demo option of int type.",
        offsetof(FroxContext, frox_int),
        AV_OPT_TYPE_INT,
        { .i64 = -1 },
        1, SHRT_MAX },
      { "frox_str",
        "This is a demo option of string type.",
        offsetof(FroxContext, frox_str),
        AV_OPT_TYPE_STRING },
      { "frox_bin",
        "This is a demo option of binary type.",
        offsetof(FroxContext, frox_bin),
        AV_OPT_TYPE_BINARY },
      { NULL },
    };

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


    Далее надо определить экземпляр типа AVClass.


    static const AVClass frox_class = {
        .class_name = "FroxContext",
        .item_name  = av_default_item_name,
        .option     = frox_options,
        .version    = LIBAVUTIL_VERSION_INT,
    };

    Указатель на этот экземпляр надо использовать для инициализации соответствующего члена AVCodec.


    AVCodec ff_frox_decoder = {
    // ...
        .priv_data_size = sizeof(FroxContext),
        .priv_class = &frox_class,
    // ...
    };
    
    AVCodec ff_frox_encoder = {
    // ...
        .priv_data_size = sizeof(FroxContext),
        .priv_class = &frox_class,
    // ...
    };

    Теперь при выполнении функции


    AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

    будет создан экземпляр структуры AVCodecContext и инициализирован член codec. Далее на основе значения codec->priv_data_size будет выделена необходимая память для экземпляра FroxContext, используя значение codec->priv_class первый член этого экземпляра будет инициализирован и после этого будет вызвана функция av_opt_set_defaults(), которая установит значений по умолчанию для опций. Указатель на экземпляр FroxContext будет доступен через член priv_data структуры AVCodecContext.


    При работе с FFmpeg API значения для опций можно установить непосредственно.


    const AVCodec *codec;
    // ...
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
    // ...
    av_opt_set(codec_ctx->priv_data, "frox_str", "meow", 0);
    av_opt_set_int(codec_ctx->priv_data, "frox_int", 42, 0);

    Другой способ — это использование словаря опций, который будет передаваться третьим аргументом при вызове avcodec_open2() (см. ниже).


    С помощью функции


    const AVOption* av_opt_next(const void* ctx, const AVOption* prev);

    можно получить список всех опций, поддерживаемых контекстом кодека. Это бывает полезно при исследовании кодека. Но перед этим надо обязательно проверить, что codec_ctx->codec->priv_class установлен в ненулевое значение, в противном случае контекст не поддерживает опций и при любой операции с опциями произойдет аварийное завершение программы.



    3.2. Функции


    Рассмотрим теперь подробнее, как устроены функции, используемые при инициализации кодека и фактического кодирования/декодирования. В них обычно всегда требуется получить указатель на FroxContext.


    AVCodecContext *codec_ctx;
    // ...
    FroxContext* frox_ctx = codec_ctx->priv_data;

    Функции frox_decode_init() и frox_encode_init() будут вызваны при выполнении функции


    int avcodec_open2(
        AVCodecContext *codec_ctx, 
        const AVCodec *codec, 
        AVDictionary **options);

    В них надо выделить необходимые ресурсы для работы кодека, и при необходимости инициализировать некоторые члены структуры AVCodecContext, например frame_size для аудиокодера.


    Функции frox_decode_close() и frox_encode_close() будут вызваны при выполнении


    int avcodec_close(AVCodecContext *codec_ctx);

    В них надо освободить выделенные ресурсы.


    Рассмотрим функцию для реализации декодирования


    int frox_decode(
        AVCodecContext *codec_ctx,
        void *outdata,
        int *outdata_size,
        AVPacket *pkt);

    Она должна реализовать следующие операции:


    1. Фактическое декодирование;
    2. Выделение необходимого буфера для выходного кадра;
    3. Копирование декодированных данных в буфер кадра.

    Рассмотрим, как надо выделять необходимый буфер для выходного кадра. Параметр outdata на самом деле указывает на AVFrame, поэтому сначала надо выполнить преобразование типа:


    AVFrame* frm = outdata;

    Далее надо выделить буфер для хранения данных кадра. Для этого надо инициализировать члены AVFrame, определяющие размер буфера кадра. Для аудио это nb_samples, channel_layout, format (для видео width, height, format).


    После этого надо вызвать функцию


    int av_frame_get_buffer(AVFrame* frm, int alignment);

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


    void av_frame_unref(AVFrame* frm);

    Функция frox_decode() должна возвращать количество байт, использованных для декодирования, из пакета, на который указывает pkt. Если формирование кадра завершено, то переменной, на которую указывает outdata_size присваивается ненулевое значение, иначе эта переменная получает значение 0.


    Рассмотрим функцию для реализации кодирования


    int frox_encode(
        AVCodecContext *codec_ctx,
        AVPacket *pkt,
        const AVFrame *frame,
        int *got_pkt_ptr);

    Она должна реализовать следующие операции:


    1. Фактическое кодирование;
    2. Выделение необходимого буфера для выходного пакета;
    3. Копирование закодированных данных в буфер пакета.

    Для выделения необходимого буфер используется функция


    int av_new_packet(AVPacket *pkt, int pack_size);

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


    void av_packet_unref(AVPacket *pkt);

    Если формирование пакета завершено, то переменной, на которую указывает got_pkt_ptr присваивается ненулевое значение, иначе эта переменная получает значение 0. В случае отсутствия ошибки, функция возвращает ноль, иначе код ошибки.


    При реализации кодека обычно используется логгирование (для ошибок это можно считать обязательным требованием). Вот пример:


    static int frox_decode_close(AVCodecContext *codec_ctx)
    {
        av_log(codec_ctx, AV_LOG_INFO, "FROX decode close\n");
    // ...
    }

    В этом случае при выводе в лог в качестве имени контекста будет использовано имя кодека.



    3.3. Метки времени


    Для задания времени в FFmpeg используется единица времени (time base), задаваемая в секундах с помощью рационального числа, представляемого типом AVRational. (Аналогичный подход используется в C++11. Например 1/1000 задает миллисекунду.) Кадры и пакеты имеют метки времени (timestamps), имеющие тип int64_t, их значения содержат время в соответствующих единицах времени. Кадр, то есть структура AVFrame, имеет член pts (presentation timestamp), значение которого определяет относительное время сцены, запечатленной в кадре. Пакет, то есть структура AVPacket, имеет члены pts (presentation timestamp) и dts (decompression timestamp). Значение dts определяет относительное время передачи пакета на декодирование. Для простых кодеков оно совпадает с pts, но для сложных кодеков может отличатся (например для h264 при использовании B-frames), то есть пакеты могут декодироваться не в том порядке в котором должны использоваться кадры.


    Единица времени определена для потока и кодека, структура AVStream имеет соответствующий член — time_base, такой же член имеет структура AVCodecContext.


    Метки времени пакета, извлеченного из потока с помощью av_read_frame(), будут заданы в единицах времени этого потока. При декодировании единица времени кодека не используется. Для видеодекодера она обычно просто не задана, для аудиодекодера имеет стандартное значение — обратное к частоте дискретизации. Декодер должен установить метку времени для выходного кадра основываясь на метках времени пакета. FFmpeg самостоятельно определяет такую метку и записывает ее в член best_effort_timestamp структуры AVFrame. Все эти метки времени будут использовать единицу времени потока, из которого извлечен пакет.


    Для кодера необходимо задавать единицу времени. В клиентском коде, организующем декодирование, надо установить значение для члена time_base структуры AVCodecContext перед вызовом avcodec_open2(). Обычно берут единицу времени, используемую для меток времени кодируемого кадра. Если этого не сделать, то видеокодеры обычно выдают ошибку, аудиокодеры устанавливают значение по умолчанию — обратное к частоте дискретизации. Может ли кодек изменить заданную единицу времени, не вполне ясно. На всякий случай лучше всегда проверять значение time_base после вызова avcodec_open2() и, если оно изменилось, пересчитывать метки времени входных кадров на единицу времени кодека. В процессе кодирования необходимо установить pts и dts пакета. После кодирования, перед записью пакета в выходной поток необходимо пересчитать метки времени пакета с единицы времени кодека на единицу времени потока. Для этого можно воспользоваться функцией


    void av_packet_rescale_ts(
        AVPacket *pkt, 
        AVRational tb_src, 
        AVRational tb_dst);

    При записи пакетов в поток необходимо гарантировать, чтобы значения dts строго возрастали, иначе мультиплексор выдаст ошибку. (Подробнее см. документацию на функцию av_interleaved_write_frame().)



    3.4. Другие функции, используемые кодеком


    При инициализации экземпляра AVCodec можно зарегистрировать еще две функции. Вот соответствующие члены AVCodec:


    typedef struct AVCodec {
    // ...
        void (*init_static_data)(AVCodec *codec);
        void (*flush)(AVCodecContext *codec_ctx);
    // ...
    } AVCodec;

    Первая из них вызывается один раз при регистрации кодека.


    Вторая сбрасывает внутреннее состояние кодека, она будет вызывается во время выполнения функции


    void avcodec_flush_buffers(AVCodecContext *codec_ctx);

    Этот вызов необходим, например, при принудительном изменении текущей позиции проигрывания.



    4. Внешняя реализация кодека



    4.1. Подключение внешней функции


    Рассмотрим следующий вариант организации кодека: кодек, зарегистрированный в FFmpeg, играет роль каркаса, а реальную процедуру кодирования/декодирования делегирует внешним функциям (своего рода plugin’ам), реализованным вне FFmpeg.


    Такого решение может быть желательно по многим причинам. Вот некоторые из них:


    1. Кодек носит экспериментальный характер и часто меняется, а компиляция FFmpeg является довольно трудоемким процессом;
    2. Кодек написан не на C, а на другом языке, например на C++;
    3. Кодек использует библиотеки или framework, которые трудно интегрировать в FFmpeg.

    Не смотря на закрытую, монолитную архитектуру FFmpeg такой вариант возможен и является вполне «законным», то есть для его реализации требуется только стандартный FFmpeg API. И ключом для решения этой задачи является механизм опций с помощью которого «внутрь» FFmpeg можно передать указатель на внешнюю функцию (или указатель структуру, содержащую указатели на внешние функции), которая и реализует требуемый функционал. Наиболее естественный вариант — это использование опций бинарного типа. В нашем примере для декодера можно предложить примерно следующее.


    typedef int(*dec_extern_t)(const void*, int, void*);
    
    static int frox_decode(
        AVCodecContext* codec_ctx,
        void* outdata,
        int *outdata_size,
        AVPacket* pkt)
    {
        int ret = -1;
        void* out_buff;
        // выделение памяти для выходного буфера out_buff
        FroxContext *fc = codec_ctx->priv_data;
        if (fc->bin_size > 0) {
            if (fc->bin_size == sizeof(dec_extern_t)) {
                dec_extern_t edec;
                memcpy(&edec, fc->frox_bin, fc->bin_size);
                ret = (*edec)(pkt->data, pkt->size, out_buff);
                if (ret >= 0) {
                // инициализация кадра и копирование out_buff в кадр
                }
            }
            else { /* ошибка */ }
        }
        else { /* декодирование по умолчанию */ }
    // ...
        return ret;
    }

    На стороне клиента FFmpeg API (в данном примере написан на C++) можно предложить примерно следующее.


    extern "C"
    {
        int DecodeFroxData(const void* buff, int size, void* outBuff);
    
        typedef int(*dec_extern_t)(const void*, int, void*);
    
        #include <libavcodec/avcodec.h>
        #include <libavutil/opt.h>
    }
    
    // ...
        AVCodecContext* ctx;
    // ...
        dec_extern_t dec = DecodeFroxData;
        void* pv = &dec;
        auto pb = static_cast<const uint8_t*>(pv);
        auto sz = sizeof(dec);
        av_opt_set_bin(ctx->priv_data, "frox_bin", pb, sz, 0);


    4.2. Внешний декодер


    Она из важных идей компьютерного мультимедиа — это отделение кодека от медиаконтейнера. В идеале медиаконтейнер любого типа может хранить медиапотоки, закодированные любым кодеком. Конечно, в реальности это не всегда выполняется. Мы видели, что для того, чтобы FFmpeg мог записать в контейнер медиапоток, мультиплексор должен «знать» кодек, так как необходимо записать идентифицирующую информацию о кодеке. А вот при чтении это уже не совсем так. Демультиплексор без проблем извлекает пакеты, закодированные неизвестным кодеком. Если клиент FFmpeg API может как-то идентифицировать этот кодек и умеет декодировать медиаданные, закодированные этим кодеком, то становится возможным воспроизведение таких медиаданных. У автора имеется подобный опыт. В свое время пришлось работать с одним видеорегистратором, который использовал аппаратное сжатие в некотором проприетарном формате. Сжатые данные переносились на PC (Windows) и затем записывались с помощью DirectShow в AVI файл. На PC имелся программный декодер для этого формата и на его основе был написан фильтр-декодер в стандарте DirectShow. Формат идентифицировался с помощью 32-битного FourCC. (Записывался в член biCompression структуры BITMAPINFOHEADER.) Таким образом, эти файлы воспроизводились на любом DirectShow проигрывателе при условии, что на PC был инсталлирован этот фильтр-декодер. При попытке воспроизвести такой файл с помощью FFmpeg проигрывателя декодер, естественно, не был найден, но член codec_tag структуры AVCodecParameters содержал вышеупомянутый FourCC, что решало проблему идентификации кодека. На основе имеющегося декодера для клиента FFmpeg API был написан дополнительный декодер, которому и передавался пакет. Таким образом проблема воспроизведения таких файлов была решена с помощью стандартной сборки FFmpeg и использования FFmpeg API.


    В ряде случаев неизвестный кодек можно идентифицировать по метаданным потока, например в *.mkv файлах FFmpeg записывает туда имя кодека (свойство ENCODER).



    Заключение


    В данной статье рассматривались только изменения в коде, не рассмотрены изменения, которые необходимо внести в другие части FFmpeg: документации, changelog, систему контроля версий и т.д. Но если вы планируете «домашнюю» сборку FFmpeg, предназначенную только для конкретного проекта, то этого можно не делать.



    Ресурсы


    Общие вопросы архитектуры FFmpeg


    [1] FFmpeg — главная страница
    [2] FFmpeg — документация
    [3] FFmpeg — Википедия
    [4] FFmpeg — русскоязычная документация по Ubuntu


    Компиляция


    [5] FFmpeg Compilation Guide
    [6] Compilation of FFmpeg 4.0 in Windows 10


    Программирование с использованием FFmpeg API


    [7] Видеоплеер на базе ffmpeg


    Добавление кодеков


    [8] FFmpeg codec HOWTO
    [9] FFmpeg video codec tutorial




    Комментарии 4

      +1
      Обычно берут единицу времени, используемую для меток времени кодируемого кадра. Если этого не сделать, то видеокодеры обычно выдают ошибку, аудиокодеры устанавливают значение по умолчанию — обратное к частоте дискретизации.
      Дело в том, что некоторые видеокодеры используют поле time_base как frame rate, некоторые делают «правильно» и берут само поле frame_rate и используют частоту кадров и частоту дискретизации времени независимо. Проблемы будут если вы нарветесь на первый вариант, например MPEG1/2 Encoder.
      Может ли кодек изменить заданную единицу времени, не вполне ясно.
      Вполне может, почему нет.
      В процессе кодирования необходимо установить pts и dts пакета.
      Тут не так все просто, на самом деле. Я считаю что dts это вообще ошибка проектирования и результат инерции мышления. В любом случае мне не известны кодеки которые бы использовали dts. Лично у меня dts сразу сбрасывается и проставляется потом, если необходима запись в контейнер. Такой подход более универсальный и удобный, на мой взгляд. Например, если мы сливаем потоки из нескольких источников в один стрим.
        0
        Для меня метки времени одна из самых таинственных частей ffmpeg. То, что я написал базируется на экспериментах, в сомнительных случаях я просто тупо вывожу метки в лог.
          +1
          Ну тут можно придерживаться примерно следующей логики:

          PTS и DTS присутствуют в пакетах и фреймах, почти всегда только для удобства транзита через некоторый воображаемый пайплайн: контейнер -> декодер -> енкодер -> контейнер. Естественно любые стадии могут отсутствовать.
          Для непосредственно процесса декодирования метки не нужны, ни PTS ни DTS. Декодер не имеет такого понятия как время, а ориентируется только на порядок семлов — temporal reference. Метки, если есть, просто проталкиваются дальше, на случай если они вдруг понадобятся плееру или енкодеру. Енкодеру DTS не нужны, так как он сам их проставляет исходя из приходящих PTS, а вот PTS могут понадобится. Как минимум, при кодировании видео в некоторые форматы необходимо выдерживать framerate или bitrate, которые высчитываются по приходящим PTS.
          DTS нужны только при записи в некоторые форматы контейнеров (типа: MOV, MP4), но точного ответа зачем, из исходников, мне пока найти не удалось, причем ffmpeg муксер ревностно следит за тем что бы DTS <= PTS.
          Да, например в контейнер MP4, в таблицу времен семплов записывается DTS. В случае если фреймы идут строго монотонно-возрастающем, по времени (I, P), то DTS совпадают с PTS. В случае если времена идут не монотонно-возрастающе (B), то добавляется еще одна корректирующая таблица, в которую записыватся разница между PTS и DTS. Но на самом деле, на практике, DTS без проблем высчитывается по PTS. У муксера даже есть дополнительный флаг на этот случай.
          В большинстве других контейнеров DTS не нужны. В DirectShow, например, который вы упомянули, понятие DTS, вообще отсутствует и это не создает никаких проблем.
            0
            Большое спасибо! О чем то я догадывался, но Вы сделали всю картину более ясной.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое