You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用FFmpeg C库编码H.264流到Matroska文件出现损坏问题

问题

使用FFmpeg C库创建仅含H.264流的Matroska(.mkv)文件时,生成的文件损坏,无法被Windows Media Player、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!
[...](该错误持续出现)

已尝试以下常见排障步骤但无效:

  • 设置AV_CODEC_FLAG_GLOBAL_HEADER
  • 手动为extradata分配空间

代码实现

#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;
}

编译与运行

  • 编译命令:
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

注:代码生成MP4文件正常,但生成MKV时损坏;用FFmpeg CLI可将正常MP4转为可用MKV:ffmpeg -i video.mp4 -c:v copy created-with-ffmpeg-cli.mkv


解决方案

你的代码存在三个关键问题,导致MKV文件损坏:

1. 错误手动分配extradata空间

设置AV_CODEC_FLAG_GLOBAL_HEADER后,编码器会自动生成并填充extradata,无需手动分配大内存块。手动分配的空extradata会覆盖编码器后续生成的有效数据,导致MKV容器无法正确解析H.264流的SPS/PPS信息。

2. avcodec_parameters_from_context调用时机错误

该函数需要在avcodec_open2之后调用,因为打开编码器后,codec_context的extradata等参数才会被正确初始化。提前调用会将未初始化的参数写入流信息,导致容器无法识别编码参数。

3. 刷新编码器时使用av_write_frame而非av_interleaved_write_frame

MKV容器需要正确的交织写入,av_write_frame不处理交织逻辑,可能导致数据包顺序错误。

修正后的代码

#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 = NULL;
    AVStream *video_stream = NULL;
    AVCodecContext *codec_context = NULL;
    int ret;

    // 初始化输出上下文
    ret = avformat_alloc_output_context2(&format_context, NULL, NULL, out_file_path);
    if (ret < 0) {
        fprintf(stderr, "Failed to allocate output context\n");
        return ret;
    }

    // 查找H.264编码器
    const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        fprintf(stderr, "H.264 encoder not found\n");
        return -1;
    }

    // 创建视频流
    video_stream = avformat_new_stream(format_context, NULL);
    if (!video_stream) {
        fprintf(stderr, "Failed to create video stream\n");
        return -1;
    }

    // 初始化编码上下文
    codec_context = avcodec_alloc_context3(codec);
    if (!codec_context) {
        fprintf(stderr, "Failed to allocate codec context\n");
        return -1;
    }

    // 设置编码参数
    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 (format_context->oformat->flags & AVFMT_GLOBALHEADER) {
        codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    // 打开编码器
    ret = avcodec_open2(codec_context, codec, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to open codec\n");
        return ret;
    }

    // 将编码参数写入流信息(必须在打开编码器后调用)
    ret = avcodec_parameters_from_context(video_stream->codecpar, codec_context);
    if (ret < 0) {
        fprintf(stderr, "Failed to copy codec parameters\n");
        return ret;
    }
    video_stream->time_base = codec_context->time_base;

    // 打开输出文件
    ret = avio_open(&format_context->pb, out_file_path, AVIO_FLAG_WRITE);
    if (ret < 0) {
        fprintf(stderr, "Failed to open output file\n");
        return ret;
    }

    // 写入文件头
    ret = avformat_write_header(format_context, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to write header\n");
        return ret;
    }

    // 创建黑色输入帧
    AVFrame *input_frame = av_frame_alloc();
    if (!input_frame) {
        fprintf(stderr, "Failed to allocate frame\n");
        return -1;
    }
    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);
    if (ret < 0) {
        fprintf(stderr, "Failed to allocate image buffers\n");
        return ret;
    }

    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);
    if (ret < 0) {
        fprintf(stderr, "Failed to fill black image\n");
        return ret;
    }

    // 写入2秒视频(60帧)
    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);
        if (ret < 0) {
            fprintf(stderr, "Failed to send frame\n");
            return ret;
        }

        AVPacket* packet = av_packet_alloc();
        if (!packet) {
            fprintf(stderr, "Failed to allocate packet\n");
            return -1;
        }

        while (1) {
            ret = avcodec_receive_packet(codec_context, packet);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else if (ret < 0) {
                fprintf(stderr, "avcodec_receive_packet failed\n");
                av_packet_free(&packet);
                return ret;
            } else {
                av_packet_rescale_ts(packet, codec_context->time_base, video_stream->time_base);
                packet->stream_index = video_stream->index;
                ret = av_interleaved_write_frame(format_context, packet);
                if (ret < 0) {
                    fprintf(stderr, "Failed to write frame\n");
                    av_packet_free(&packet);
                    return ret;
                }
                av_packet_unref(packet);
            }
        }

        av_packet_free(&packet);
    }

    av_frame_free(&input_frame);

    // 刷新编码器
    ret = avcodec_send_frame(codec_context, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to send flush frame\n");
        return ret;
    }

    AVPacket *flush_packet = av_packet_alloc();
    if (!flush_packet) {
        fprintf(stderr, "Failed to allocate flush packet\n");
        return -1;
    }

    while (1) {
        ret = avcodec_receive_packet(codec_context, flush_packet);
        if (ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            fprintf(stderr, "Failed to receive flush packet\n");
            av_packet_free(&flush_packet);
            return ret;
        }

        av_packet_rescale_ts(flush_packet, codec_context->time_base, video_stream->time_base);
        flush_packet->stream_index = video_stream->index;
        ret = av_interleaved_write_frame(format_context, flush_packet);
        if (ret < 0) {
            fprintf(stderr, "Failed to write flush frame\n");
            av_packet_free(&flush_packet);
            return ret;
        }
        av_packet_unref(flush_packet);
    }

    av_packet_free(&flush_packet);

    // 写入文件尾并关闭资源
    ret = av_write_trailer(format_context);
    if (ret < 0) {
        fprintf(stderr, "Failed to write trailer\n");
        return ret;
    }

    avio_close(format_context->pb);
    avcodec_free_context(&codec_context);
    avformat_free_context(format_context);

    return 0;
}

关键修改点说明

  • 移除手动分配extradata的代码,让编码器自动生成并填充全局头数据
  • 将avcodec_parameters_from_context移至avcodec_open2之后调用,确保编码参数已正确初始化
  • 刷新编码器时统一使用av_interleaved_write_frame,保证MKV容器的交织写入逻辑正确
  • 通过AVFMT_GLOBALHEADER自动判断容器是否需要全局头,替代硬编码文件名的判断方式
  • 添加基础错误处理,便于排查运行时问题

内容的提问来源于stack exchange,提问作者Marvin Killing

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 23:47:57