Библиотека C FFMPEG: кодирование потока h264 в контейнер Matroska .mkv приводит к повреждению файлов

Я хочу использовать библиотеку C FFMPEG для создания файла Matroska Video .mkv только с потоком h264, но полученный файл .mkv оказывается поврежденным.

Файл невозможно воспроизвести с помощью проигрывателя Windows Media, ffplay или VLC, и когда я пытаюсь ffprobe получить полученный файл, появляются следующие сообщения об ошибках:

[h264 @ 0000015060d8f5c0] No start code is found.
    Last message repeated 1 times
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
    Last message repeated 1 times
[h264 @ 0000015060d8f5c0] decode_slice_header error
[h264 @ 0000015060d8f5c0] no frame!
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
    Last message repeated 1 times
[h264 @ 0000015060d8f5c0] decode_slice_header error
[h264 @ 0000015060d8f5c0] no frame!
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
    Last message repeated 1 times
# [...]
# this continues for a long time

Я выполнил другие шаги по устранению неполадок при кодировании h264 в Matroska, но ни один из них, похоже, не помог мне:

Это мой код C:

#include <stdio.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>

int main(void) {
    char *out_file_path = "./video.mkv";
    AVFormatContext *format_context;
    AVStream *video_stream;
    AVCodecContext *codec_context;

    avformat_alloc_output_context2(&format_context, NULL, NULL, out_file_path);

    const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    video_stream = avformat_new_stream(format_context, NULL);

    codec_context = avcodec_alloc_context3(codec);
    av_opt_set(codec_context->priv_data, "preset", "superfast", 0);
    av_opt_set(codec_context->priv_data, "crf", "22", 0);

    codec_context->width = 1920;
    codec_context->height = 1080;
    codec_context->time_base = av_make_q(1, 30);
    codec_context->pix_fmt = AV_PIX_FMT_YUV420P;

    if (strncmp("./video.mkv", out_file_path, 11) == 0) {
      printf("Writing .mkv...\n");
      codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
      codec_context->extradata = (uint8_t*)av_mallocz(1024 * 1024);
      codec_context->extradata_size = 1024 * 1024;
    } else {
      printf("Writing .mp4...\n");
    }

    // XXX avcodec_parameters_from_context is potentially superfluous (?)
    int ret = avcodec_parameters_from_context(video_stream->codecpar, codec_context);

    ret = avcodec_open2(codec_context, codec, NULL);

    avio_open(&format_context->pb, out_file_path, AVIO_FLAG_WRITE);

    ret = avformat_write_header(format_context, NULL);

    // create a black input frame
    AVFrame *input_frame = av_frame_alloc();
    input_frame->width = 1920;
    input_frame->height = 1080;
    input_frame->format = AV_PIX_FMT_YUV420P;
    ret = av_image_alloc(input_frame->data, input_frame->linesize, input_frame->width, input_frame->height, input_frame->format, 32);
    ptrdiff_t linesize[4] = { input_frame->linesize[0], input_frame->linesize[1], input_frame->linesize[2], input_frame->linesize[3] };
    ret = av_image_fill_black(input_frame->data, linesize, input_frame->format, 0, 1920, 1080);

    // write 2 seconds of video, all black
    for (size_t current_pts = 0; current_pts < 60; current_pts++) {
      input_frame->pts = av_rescale_q(current_pts, av_make_q(1, 30), codec_context->time_base);;

      ret = avcodec_send_frame(codec_context, input_frame);

      AVPacket* packet = av_packet_alloc();

      while (1) {
          ret = avcodec_receive_packet(codec_context, packet);
          if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
              break;
          } else if (ret < 0) {
              printf("avcodec_receive_packet failed");
          } else {
              av_packet_rescale_ts(packet, codec_context->time_base, video_stream->time_base);
              ret = av_interleaved_write_frame(format_context, packet);
          }
      }

      av_packet_free(&packet);
    }

    av_frame_free(&input_frame);

    // flush encoder
    avcodec_send_frame(codec_context, NULL);
    AVPacket *flush_packet = av_packet_alloc();
    while (avcodec_receive_packet(codec_context, flush_packet) != AVERROR_EOF) {
        //int ret = av_interleaved_write_frame(format_context_, packet);
        av_packet_rescale_ts(flush_packet, codec_context->time_base, video_stream->time_base);
        ret = av_write_frame(format_context, flush_packet);
    }
    av_packet_free(&flush_packet);

    ret = av_write_trailer(format_context);
    ret = avio_close(format_context->pb);

    return 0;
}

Обработка ошибок удалена, но она не вызывает никаких ошибок.

Этот код работает, когда я записываю в файл mp4, но создает поврежденный файл при записи в .mkv. Вы можете изменить формат выходного контейнера на mp4, установив char *out_file_path = "./video.mp4";

Вы можете скомпилировать это, запустив

clang -I$(FFMPEG_DIR)/include -L$(FFMPEG_DIR)/lib -lavformat -lavcodec -lavutil main.c -o makemkv

Вывод, который я получаю во время выполнения приведенного выше кода:

Writing .mkv...
[libx264 @ 0x139804c40] using cpu capabilities: ARMv8 NEON
[libx264 @ 0x139804c40] profile High, level 4.0, 4:2:0, 8-bit
[libx264 @ 0x139804c40] 264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=1 deblock=1:0:0 analyse=0x3:0x3 me=dia subme=1 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=15 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=1 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc=crf mbtree=0 crf=22.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 pb_ratio=1.30 aq=1:1.00

Когда я использую CLI ffmpeg, я могу создать рабочий .mkv из моего рабочего .mp4 следующим образом:

ffmpeg -i video.mp4 -c:v copy created-with-ffmpeg-cli.mkv

Полученные видеофайлы я загрузил сюда: https://drive.google.com/drive/folders/1FS-0fBAwKBbO-tyxC0VrFqcCyyqd0BR_?usp=sharing

@TedLyngmo Я неправильно понял, как следует использовать систему тегов, сейчас я удалил тег.

Marvin Killing 11.03.2024 11:21

Хорошо. Я добавил тег c++, иначе не думаю, что у вас будет много читателей :-)

Ted Lyngmo 11.03.2024 13:06

Просто мысль (сомневаюсь, что это настоящая проблема), вы настроили GOP на 1, по сути, конвертируя H264 в MJPEG. Возможно, это вызывает некоторую путаницу?

Alon Catz 12.03.2024 16:26

Пожалуйста, опубликуйте весь свой код, почему существует рамка RGB и масштабирование, когда вы просто пытаетесь мультиплексировать поток juv h264 в контейнер mkv? В выводе декодера говорится, что конечных единиц нет, например, нет данных h264, таких как SPS, PPS и т. д.

Christoph 13.03.2024 20:30

@MarvinKilling (1) Хорошо ли работает версия для командной строки с точно таким же вводом H264 для создания работающего MKV? (2) Судя по коду C++, имеет ли выходной MKV меньший размер, чем входные данные H264? Просто интересно, правильно ли настроен контейнер, но почему-то данные кадра на самом деле не были записаны. (3) Можете ли вы создать тестовый файл, чтобы поделиться им для нашего собственного тестирования воспроизведения? Достаточно простого цветного квадрата 64x64. Пожалуйста, закодируйте это изображение в h264 (это будет один кадр), а затем мультиплексируйте с вашим кодом. Поделитесь 3 файлами: (а) сам H264, (б) как мультиплексированный в MKV и (в) как мультиплексированный в MP4.

VC.One 14.03.2024 20:55

@MarvinKilling PS: Если вы сможете выполнить третий пункт выше, вы быстрее получите ответ о проблеме в данных выходного файла от других, кто может проверить тот же вывод. PPS: Вы открыты для советов/ответов по поводу ручного мультиплексирования H264 в MKV? Я имею в виду, что для вас является более приоритетным в этой задаче? Речь идет об обучении правильному использованию FFmpeg в C++? Или вы просто хотите знать, как вчера доставить MKV с байтами H264 (готовы написать собственный код)?

VC.One 14.03.2024 21:10

Эй, спасибо всем! Я переписал свой код выше, чтобы он стал полным примером на простом C (больше не на C++), и загрузил видеофайлы, которые он создает, на Google Диск. Интересно то, что создание .mp4 с почти такой же конфигурацией потока h264 работает. Я почти уверен, что проблема вызвана этим кодом: codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; codec_context->extradata = (uint8_t*)av_mallocz(1024 * 1024); codec_context->extradata_size = 1024 * 1024; Однако, если я его оставлю, avformat_write_header потерпит неудачу.

Marvin Killing 15.03.2024 19:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
7
245
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

[h264 @ 0000015060d8f5c0] No start code is found.
Last message repeated 1 times
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
Last message repeated 1 times
[h264 @ 0000015060d8f5c0] decode_slice_header error
[h264 @ 0000015060d8f5c0] no frame!
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
Last message repeated 1 times
[h264 @ 0000015060d8f5c0] decode_slice_header error
[h264 @ 0000015060d8f5c0] no frame!
[h264 @ 0000015060d8f5c0] non-existing PPS 0 referenced
Last message repeated 1 times

Похоже, ваш кодер H.264 не устанавливает ключевые кадры с этими настройками. Пожалуйста, изучите пример кода здесь: https://ffmpeg.org//doxygen/trunk/encode_video_8c-example.html

Исправить типы данных:

Меня беспокоит эта строка:

codec_context_->time_base = av_make_q(1, 90000);

Почему бы вам не использовать тип AVRational, как это предложено в документации?

time_base = (AVRational){1, 25};
framerate = (AVRational){25, 1};

Нет никаких препятствий, чтобы проверить, кажется ли ваша частота кадров постоянной.

Для H.264 с потерями размер GOP будет больше 1:

    c->gop_size = 10;
    c->max_b_frames = 1;
    c->pix_fmt = AV_PIX_FMT_YUV420P;

Исправить временные метки:

Больше всего меня беспокоит то, что вы никогда не увеличиваете поле pts, поэтому каждый кадр в последовательности должен иметь одинаковую временную метку, поскольку функция av_packet_rescale_ts только масштабирует действительную временную метку, но не увеличивает и не пересчитывает ее. Таким образом, все кадры имеют временную метку 0, поскольку она никогда не устанавливалась в соответствии с существующей частотой кадров.

Привет, извините, если мой код, опубликованный выше, вызвал недоразумения. Я правильно настроил точки и временные рамки, код без проблем работал при создании потока h264 в контейнере mp4. av_make_q возвращает AVRational и должно быть эквивалентным. time_base был выбран из-за варианта использования моего кода: входящие кадры имеют метки времени в микросекундах и в противном случае могут перекрываться. Но это не относится к данной проблеме. Теперь я сократил свой код до минимального воспроизводимого примера и удалил неинтуитивные части.

Marvin Killing 15.03.2024 19:36
Ответ принят как подходящий

Проблема оказалась в этой строке:

codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

Если его удалить, то все работает нормально.

Важно выделить немного места для таких дополнительных данных:

codec_context->extradata = (uint8_t*)av_mallocz(32);
codec_context->extradata_size = 32;

Вы должны обнулить дополнительные данные (например, присвоив их av_mallocz) и убедиться, что их размер равен 6 или больше. Фактический размер, похоже, не имеет значения, если он равен 6 или больше.

Это отличается от записи в контейнер mp4, где дополнительные данные не имеют значения.

Я не понимаю, почему эта строка соотносится с 0, написанным в pts. Требует ли спецификация MKV AV_CODEC_FLAG_GLOBAL_HEADER и расширенных метаданных?

Andrei Vukolov 19.03.2024 17:18

Другие вопросы по теме