无法通过libavcodec添加tmcd流以复现ffmpeg -timecode选项的效果
无法通过libavcodec添加tmcd流以复现ffmpeg -timecode选项的效果
看起来你在尝试用libav库实现类似ffmpeg -timecode的功能,给MOV文件添加tmcd时间码轨道,但遇到了流不写入输出文件的问题。我帮你分析下代码里的几个关键问题,然后给出修正后的完整实现:
你的代码中存在的核心问题
- Codec ID选择错误:你给tmcd流设置了
AV_CODEC_ID_TIMED_ID3,但MOV容器中的tmcd轨道是自定义数据类型,不需要绑定特定编解码器,应该用AV_CODEC_ID_NONE。 - 空流被容器丢弃:即使创建了tmcd流,如果没有向其中写入至少一个数据包,MOV容器会自动忽略这个空流——这是你看不到它的主要原因。
- 代码未完成完整写入流程:你的代码在创建AVFrame后就截断了,没有完成视频帧的编码、写入,也没有调用
av_write_trailer()收尾,这会导致文件不完整,流信息也无法正确保存。
修正后的完整代码
#include <iostream> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> #include <libswscale/swscale.h> #include <libavutil/opt.h> #include <libavutil/imgutils.h> #include <libavutil/samplefmt.h> } bool checkProResAvailability() { const AVCodec* codec = avcodec_find_encoder_by_name("prores_ks"); if (!codec) { std::cerr << "ProRes codec not available. Please install FFmpeg with ProRes support." << std::endl; return false; } return true; } // 给tmcd流写入一个空数据包(必须,否则容器会丢弃空流) int writeTimecodePacket(AVFormatContext* fmtCtx, AVStream* tmcdStream) { AVPacket pkt = {0}; av_init_packet(&pkt); pkt.stream_index = tmcdStream->index; pkt.pts = pkt.dts = 0; pkt.duration = 1; pkt.flags |= AV_PKT_FLAG_KEY; int ret = av_interleaved_write_frame(fmtCtx, &pkt); av_packet_unref(&pkt); return ret; } int main(){ av_log_set_level(AV_LOG_INFO); const char* outputFileName = "test_tmcd.mov"; AVFormatContext* formatContext = nullptr; AVCodecContext* videoCodecContext = nullptr; AVStream* videoStream = nullptr; AVStream* timecodeStream = nullptr; if (!checkProResAvailability()) { return -1; } std::cout << "Creating test file with tmcd stream: " << outputFileName << std::endl; // 分配输出格式上下文 if (avformat_alloc_output_context2(&formatContext, nullptr, "mov", outputFileName) < 0) { std::cerr << "Failed to allocate output context!" << std::endl; return -1; } if (avio_open(&formatContext->pb, outputFileName, AVIO_FLAG_WRITE) < 0) { std::cerr << "Failed to open output file!" << std::endl; avformat_free_context(formatContext); return -1; } // 查找ProRes编码器 const AVCodec* videoCodec = avcodec_find_encoder_by_name("prores_ks"); if (!videoCodec) { std::cerr << "Failed to find the ProRes encoder!" << std::endl; avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 初始化视频流 videoStream = avformat_new_stream(formatContext, nullptr); if (!videoStream) { std::cerr << "Failed to create video stream!" << std::endl; avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } videoCodecContext = avcodec_alloc_context3(videoCodec); if (!videoCodecContext) { std::cerr << "Failed to allocate video codec context!" << std::endl; avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 配置视频编码器参数 videoCodecContext->width = 1920; videoCodecContext->height = 1080; videoCodecContext->pix_fmt = AV_PIX_FMT_YUV422P10; videoCodecContext->time_base = (AVRational){1, 30}; // 30FPS videoCodecContext->framerate = (AVRational){30, 1}; videoCodecContext->bit_rate = 20000000; // 合理的ProRes码率 videoCodecContext->gop_size = 1; // 每帧都是I帧 // 打开编码器 if (avcodec_open2(videoCodecContext, videoCodec, nullptr) < 0) { std::cerr << "Failed to open ProRes codec!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 复制编码器参数到视频流 if (avcodec_parameters_from_context(videoStream->codecpar, videoCodecContext) < 0) { std::cerr << "Failed to copy codec parameters to video stream!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } videoStream->time_base = videoCodecContext->time_base; videoStream->avg_frame_rate = videoCodecContext->framerate; // -------------------------- 修正后的时间码流设置 -------------------------- timecodeStream = avformat_new_stream(formatContext, nullptr); if (!timecodeStream) { std::cerr << "Failed to create timecode stream!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // tmcd是自定义数据轨道,无需绑定编解码器 timecodeStream->codecpar->codec_type = AVMEDIA_TYPE_DATA; timecodeStream->codecpar->codec_id = AV_CODEC_ID_NONE; timecodeStream->codecpar->codec_tag = MKTAG('t', 'm', 'c', 'd'); // tmcd标识 timecodeStream->time_base = (AVRational){1, 30}; // 和视频流时间基准保持一致 // 设置时间码元数据 if (av_dict_set(&timecodeStream->metadata, "timecode", "00:00:30:00", 0) < 0) { std::cerr << "Failed to set timecode metadata!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // ------------------------------------------------------------------------- // 写入文件头 if (avformat_write_header(formatContext, nullptr) < 0) { std::cerr << "Failed to write file header!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 给tmcd流写入空数据包(必须步骤,否则容器会丢弃空流) if (writeTimecodePacket(formatContext, timecodeStream) < 0) { std::cerr << "Failed to write timecode packet!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 生成测试视频帧 AVFrame* frame = av_frame_alloc(); if (!frame) { std::cerr << "Failed to allocate video frame!" << std::endl; avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } frame->format = videoCodecContext->pix_fmt; frame->width = videoCodecContext->width; frame->height = videoCodecContext->height; // 分配帧缓冲区 if (av_frame_get_buffer(frame, 0) < 0) { std::cerr << "Failed to allocate frame buffer!" << std::endl; av_frame_free(&frame); avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 填充灰帧测试数据 for (int i = 0; i < videoCodecContext->height; i++) { memset(frame->data[0] + i * frame->linesize[0], 128, frame->linesize[0]); memset(frame->data[1] + i * frame->linesize[1], 128, frame->linesize[1]); memset(frame->data[2] + i * frame->linesize[2], 128, frame->linesize[2]); } frame->pts = 0; // 设置帧时间戳 // 发送帧到编码器 if (avcodec_send_frame(videoCodecContext, frame) < 0) { std::cerr << "Failed to send frame to encoder!" << std::endl; av_frame_free(&frame); avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 接收编码后的数据包并写入文件 AVPacket pkt = {0}; av_init_packet(&pkt); int ret = avcodec_receive_packet(videoCodecContext, &pkt); if (ret == 0) { av_packet_rescale_ts(&pkt, videoCodecContext->time_base, videoStream->time_base); pkt.stream_index = videoStream->index; av_interleaved_write_frame(formatContext, &pkt); av_packet_unref(&pkt); } else if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { std::cerr << "Failed to receive encoded packet!" << std::endl; av_frame_free(&frame); avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); return -1; } // 刷新编码器剩余数据 avcodec_send_frame(videoCodecContext, nullptr); while (ret >= 0) { ret = avcodec_receive_packet(videoCodecContext, &pkt); if (ret == AVERROR_EOF) break; else if (ret < 0) { std::cerr << "Error during encoder flush!" << std::endl; av_packet_unref(&pkt); break; } av_packet_rescale_ts(&pkt, videoCodecContext->time_base, videoStream->time_base); pkt.stream_index = videoStream->index; av_interleaved_write_frame(formatContext, &pkt); av_packet_unref(&pkt); } // 写入文件尾 av_write_trailer(formatContext); // 释放所有资源 av_frame_free(&frame); avcodec_free_context(&videoCodecContext); avio_close(formatContext->pb); avformat_free_context(formatContext); std::cout << "File created successfully! Check " << outputFileName << " for tmcd stream." << std::endl; return 0; }
关键修改说明
修正时间码流的Codec ID:
把AV_CODEC_ID_TIMED_ID3改成AV_CODEC_ID_NONE,因为tmcd是MOV容器的自定义数据轨道,不需要绑定任何编解码器。添加tmcd流数据包写入逻辑:
新增了writeTimecodePacket函数,给tmcd流写入一个空的关键数据包。这是核心步骤——MOV容器会自动忽略没有任何数据包的空流,写入至少一个包才能让轨道被保留。完善视频流参数配置:
补充了framerate、gop_size等参数,调整ProRes码率到更合理的20Mbps,确保编码器稳定工作。完整的文件写入流程:
补全了帧缓冲区分配、测试数据填充、编码器刷新、文件尾写入等步骤,确保文件完整可播放,所有流都被正确保存。
验证方法
编译运行代码后,用ffprobe查看输出文件:
ffprobe test_tmcd.mov
你应该能看到类似这样的tmcd流信息,说明轨道已经成功添加,和ffmpeg -timecode的效果完全一致:
Stream #0:1(eng): Data: none (tmcd / 0x64636D74), 0 kb/s Metadata: timecode : 00:00:30:00




