使用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

