You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

无法通过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;
}

关键修改说明

  1. 修正时间码流的Codec ID
    AV_CODEC_ID_TIMED_ID3改成AV_CODEC_ID_NONE,因为tmcd是MOV容器的自定义数据轨道,不需要绑定任何编解码器。

  2. 添加tmcd流数据包写入逻辑
    新增了writeTimecodePacket函数,给tmcd流写入一个空的关键数据包。这是核心步骤——MOV容器会自动忽略没有任何数据包的空流,写入至少一个包才能让轨道被保留。

  3. 完善视频流参数配置
    补充了framerategop_size等参数,调整ProRes码率到更合理的20Mbps,确保编码器稳定工作。

  4. 完整的文件写入流程
    补全了帧缓冲区分配、测试数据填充、编码器刷新、文件尾写入等步骤,确保文件完整可播放,所有流都被正确保存。


验证方法

编译运行代码后,用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

火山引擎 最新活动