FFmpeg8.0.1 源代码的深入分析

执行命令 -i input.mp4 -vframes 1 -f image2 -y output.jpg

🎯 完整调用链概览

main()

├── 初始化阶段

│ ├── init_dynload()

│ ├── parse_loglevel()

│ ├── avdevice_register_all()

│ ├── avformat_network_init()

│ ├── show_banner()

│ └── sch_alloc()

├── 参数解析阶段 [ffmpeg_parse_options()]

│ ├── split_commandline() - 分割命令行参数

│ ├── parse_optgroup() - 解析全局选项

│ ├── term_init() - 初始化终端

│ ├── open_files(..., ifile_open) - 打开输入文件

│ └── open_files(..., of_open) - 打开输出文件

└── 转码执行阶段 [transcode()]

├── sch_start() - 启动调度器

├── sch_wait() - 等待处理完成

├── sch_stop() - 停止调度器

└── of_write_trailer() - 写入尾部

📋 分阶段详细调用链

  1. 程序入口 (fftools/ffmpeg.c:964)

    main(int argc, char **argv)

    ├── init_dynload() // 初始化动态加载

    ├── setvbuf(stderr, ...) // 设置stderr缓冲

    ├── av_log_set_flags(AV_LOG_SKIP_REPEATED)

    ├── parse_loglevel(argc, argv, options) // 解析日志级别

    ├── avdevice_register_all() // 注册所有设备 [CONFIG_AVDEVICE]

    ├── avformat_network_init() // 初始化网络

    ├── show_banner(argc, argv, options) // 显示版本信息 Banner

    └── sch_alloc() // 分配调度器

  2. 参数解析阶段 (fftools/ffmpeg_opt.c:1380)

    ffmpeg_parse_options(argc, argv, sch)

    ├── split_commandline(&octx, argc, argv, ...)

    │ // 将命令行参数分割为内部表示

    │ // 识别: -i input.mp4 -vframes 1 -f image2 -y output.jpg

    ├── parse_optgroup(&go, &octx.global_opts, options)

    │ // 解析全局选项 (如 -y 覆盖输出)

    ├── term_init()

    │ // 配置终端和信号处理

    ├── open_files(&octx.groups[GROUP_INFILE], "input", sch, ifile_open)

    │ // 打开所有输入文件 ↓↓↓

    └── open_files(&octx.groups[GROUP_OUTFILE], "output", sch, of_open)

    // 打开所有输出文件 ↓↓↓

  3. 输入文件打开 (fftools/ffmpeg_demux.c:1656)

    ifile_open(o, filename="/Users/.../input.mp4", sch)

    ├── demux_alloc() // 分配解复用器

    ├── sch_add_demux(sch, input_thread, d) // 添加解复用线程

    ├── av_find_input_format(o->format) // 查找输入格式 (NULL时自动检测)

    ├── avformat_alloc_context() // 分配格式上下文

    │ └── ic = AVFormatContext* // 创建输入上下文

    ├── avformat_open_input(&ic, filename, file_iformat, &format_opts)

    │ // ★★★ 打开输入文件,探测格式 ★★★

    │ │

    │ └── [libavformat/demux.c] avformat_open_input()

    │ ├── init_input() // 初始化输入

    │ │ ├── av_probe_input_format() // 探测文件格式

    │ │ └── avio_open2() // 打开IO

    │ ├── s->iformat->read_header() // 读取文件头 (mov_read_header for MP4)

    │ └── avformat_find_stream_info() // 查找流信息

    ├── avformat_find_stream_info(ic, ...)

    │ // ★★★ 分析流信息,读取若干帧以确定编码参数 ★★★

    │ │

    │ └── [libavformat/demux.c]

    │ ├── av_read_frame() // 读取帧

    │ ├── try_decode_frame() // 尝试解码帧

    │ └── estimate_timings() // 估算时长

    └── ist_add() // 添加输入流

    └── dec_create() // 创建解码器

    └── dec_open() // 打开解码器

    └── avcodec_open2() // 打开编解码器

关键 libavformat 函数:

avformat_open_input() (libavformat/demux.c) - 打开文件并探测格式

avformat_find_stream_info() - 分析流参数

格式特定函数如 mov_read_header() (libavformat/mov.c) - 读取 MP4 文件头

  1. 输出文件打开 (fftools/ffmpeg_mux_init.c:3262)

of_open(o, filename="/Users/.../xx.jpg", sch)

├── mux_alloc() // 分配复用器

├── avformat_alloc_output_context2(&oc, NULL, o->format="image2", filename)

│ // ★★★ 分配输出上下文,根据 -f image2 和文件名确定格式 ★★★

│ │

│ └── [libavformat/mux.c]

│ ├── av_guess_format("image2", filename, NULL)

│ │ // 根据格式名或文件扩展名猜测输出格式

│ └── avformat_alloc_context()

├── sch_add_mux(sch, muxer_thread, ...)

│ // 添加复用线程到调度器

├── create_streams(mux, o) // 创建输出流

│ └── new_output_stream(mux, o, AVMEDIA_TYPE_VIDEO, ...)

│ ├── ost_add() // 添加输出流

│ └── enc_open() // 打开编码器

│ ├── avcodec_find_encoder() // 查找编码器 (mjpeg)

│ ├── avcodec_alloc_context3()// 分配编码器上下文

│ └── avcodec_open2() // 打开编码器

│ └── [libavcodec/encode.c] ff_encode_open()

└── avio_open2(&oc->pb, filename, AVIO_FLAG_WRITE, ...)

// ★★★ 打开输出文件进行写入 ★★★

关键编码器:

查找到的编码器: mjpeg (Motion JPEG)

编码器实现: libavcodec/mjpegenc.c

  1. 转码主循环 (fftools/ffmpeg.c:870)

transcode(sch)

├── print_stream_maps() // 打印流映射信息

├── sch_start(sch) // ★★★ 启动调度器,开始所有线程 ★★★

│ │

│ ├── 启动 input_thread() 线程 ↓ // 解复用线程

│ ├── 启动 decoder_thread() 线程 ↓ // 解码线程

│ ├── 启动 encoder_thread() 线程 ↓ // 编码线程

│ └── 启动 muxer_thread() 线程 ↓ // 复用线程

├── while (!sch_wait(sch, ...))

│ ├── check_keyboard_interaction() // 检查键盘输入 (q退出)

│ └── print_report() // 打印进度报告

├── sch_stop(sch, &transcode_ts) // 停止调度器

└── of_write_trailer() // 写入文件尾部

└── av_write_trailer(oc)

  1. 解复用线程 (fftools/ffmpeg_demux.c:717)

    input_thread(void *arg) // 在独立线程中运行

    ├── demux_thread_init(&dt) // 初始化线程上下文

    ├── while (1) { // 主循环

    │ │

    │ ├── av_read_frame(f->ctx, dt.pkt_demux)

    │ │ // ★★★ 从输入文件读取一个数据包 ★★★

    │ │ │

    │ │ └── [libavformat/demux.c] av_read_frame()

    │ │ ├── ff_read_packet() // 读取原始包

    │ │ │ └── s->iformat->read_packet() // 格式特定读取 (mov)

    │ │ └── parse_packet() // 解析数据包

    │ │

    │ ├── input_packet_process(d, pkt, ...)

    │ │ // 处理输入包 (时间戳调整等)

    │ │

    │ └── demux_send(d, &dt, ds, pkt, ...)

    │ └── sch_demux_send() // 发送包到调度器

    │ }

    └── demux_thread_uninit(&dt)

  2. 解码流程 (fftools/ffmpeg_dec.c)

    decoder_thread(void *arg) // 在独立线程中运行

    ├── while (接收数据包) {

    │ │

    │ ├── sch_dec_receive() // 从调度器接收包

    │ │

    │ ├── dec_packet(...)

    │ │ ├── packet_decode(d, pkt, frame)

    │ │ │ │

    │ │ │ ├── avcodec_send_packet(dec, pkt)

    │ │ │ │ // ★★★ 发送压缩包给解码器 ★★★

    │ │ │ │ │

    │ │ │ │ └── [libavcodec/decode.c] avcodec_send_packet()

    │ │ │ │ └── ff_decode_preinit()

    │ │ │ │

    │ │ │ └── avcodec_receive_frame(dec, frame)

    │ │ │ // ★★★ 从解码器接收解码后的帧 ★★★

    │ │ │ │

    │ │ │ └── [libavcodec/decode.c] avcodec_receive_frame()

    │ │ │ ├── decode_simple_internal()

    │ │ │ │ └── codec->cb.decode() // H.264解码器

    │ │ │ │ └── [libavcodec/h264dec.c] h264_decode_frame()

    │ │ │ └── av_frame_ref()

    │ │ │

    │ │ └── sch_dec_send() // 发送解码后的帧

    │ }

    └── 对于 -vframes 1: 解码第1帧后退出

关键解码器:

H.264 解码器: libavcodec/h264dec.c

核心函数: h264_decode_frame()

  1. 编码流程 (fftools/ffmpeg_enc.c)

encoder_thread(void *arg) // 在独立线程中运行

├── while (接收帧) {

│ │

│ ├── sch_enc_receive() // 从调度器接收帧

│ │

│ ├── enc_frame(enc, frame)

│ │ ├── do_video_out(e, frame, ...)

│ │ │ │

│ │ │ ├── avcodec_send_frame(enc, frame)

│ │ │ │ // ★★★ 发送原始帧给编码器 ★★★

│ │ │ │ │

│ │ │ │ └── [libavcodec/encode.c] avcodec_send_frame()

│ │ │ │ └── ff_encode_preinit()

│ │ │ │

│ │ │ └── avcodec_receive_packet(enc, pkt)

│ │ │ // ★★★ 从编码器接收编码后的包 ★★★

│ │ │ │

│ │ │ └── [libavcodec/encode.c] avcodec_receive_packet()

│ │ │ ├── encode_simple_internal()

│ │ │ │ └── codec->cb.encode() // MJPEG编码器

│ │ │ │ └── [libavcodec/mjpegenc.c] ff_mpv_encode_picture()

│ │ │ └── av_packet_ref()

│ │ │

│ │ └── sch_enc_send() // 发送编码后的包

│ }

└── 对于 -vframes 1: 编码1帧后发送EOF

关键编码器:

MJPEG 编码器: libavcodec/mjpegenc.c

核心函数: ff_mpv_encode_picture()

  1. 复用线程 (fftools/ffmpeg_mux.c:407)

muxer_thread(void *arg) // 在独立线程中运行

├── mux_thread_init(&mt) // 初始化复用线程

├── while (1) {

│ │

│ ├── sch_mux_receive(mux->sch, ...) // 从调度器接收编码后的包

│ │

│ ├── mux_packet_filter(mux, &mt, ost, pkt, ...)

│ │ ├── of_streamcopy() // 流复制处理

│ │ └── write_packet(of, pkt)

│ │ ├── of_check_init(of)

│ │ │ └── avformat_write_header(s, ...)

│ │ │ // ★★★ 写入文件头 (仅首次调用) ★★★

│ │ │ │

│ │ │ └── [libavformat/mux.c] avformat_write_header()

│ │ │ └── s->oformat->write_header()

│ │ │ └── [libavformat/img2enc.c] write_header()

│ │ │

│ │ └── av_interleaved_write_frame(s, pkt)

│ │ // ★★★ 写入数据帧 ★★★

│ │ │

│ │ └── [libavformat/mux.c] av_interleaved_write_frame()

│ │ ├── interleaved_write_packet()

│ │ │ └── s->oformat->write_packet()

│ │ │ └── [libavformat/img2enc.c] write_packet()

│ │ └── avio_write() // 实际IO写入

│ }

└── mux_thread_uninit(&mt)

关键复用器:

image2 复用器: libavformat/img2enc.c

核心函数: write_packet() - 将JPEG数据写入文件

  1. 清理和退出

transcode() 返回后

├── ffmpeg_cleanup(ret) // 清理资源

│ ├── 关闭所有输入文件

│ ├── 关闭所有输出文件

│ ├── 释放编解码器上下文

│ └── 释放格式上下文

├── sch_free(&sch) // 释放调度器

└── return ret // 退出程序

🔑 核心库函数调用链

libavformat (格式处理)

输入: avformat_open_input() → avformat_find_stream_info() → av_read_frame()

输出: avformat_alloc_output_context2() → avformat_write_header() → av_interleaved_write_frame() → av_write_trailer()

libavcodec (编解码)

解码: avcodec_find_decoder() → avcodec_open2() → avcodec_send_packet() → avcodec_receive_frame()

编码: avcodec_find_encoder() → avcodec_open2() → avcodec_send_frame() → avcodec_receive_packet()

libavutil (工具函数)

内存: av_malloc(), av_free(), av_packet_alloc(), av_frame_alloc()

日志: av_log()

时间: av_gettime_relative(), av_rescale_q()

📊 数据流向

输入文件 (MP4)

↓ av_read_frame()

H.264 编码包

↓ avcodec_send_packet() / avcodec_receive_frame()

YUV420P 原始帧 (960x1280)

↓ avcodec_send_frame() / avcodec_receive_packet()

MJPEG 编码包

↓ av_interleaved_write_frame()

输出文件 (JPEG)

⚙️ 线程模型

FFmpeg 使用调度器 (Scheduler) 管理多个工作线程:

input_thread - 读取输入文件并解复用

decoder_thread - 解码 H.264 → YUV

encoder_thread - 编码 YUV → MJPEG

muxer_thread - 写入输出文件

线程间通过调度器队列传递数据包和帧

相关推荐
小曾同学.com7 小时前
音视频中的“透传”与“DTS音频”
ffmpeg·音视频·透传·dts
vivo互联网技术7 小时前
数字人动画云端渲染方案
前端·ffmpeg·puppeteer·web3d
止礼8 小时前
FFmpeg8.0.1 编解码流程
ffmpeg
qs70169 小时前
c直接调用FFmpeg命令无法执行问题
c语言·开发语言·ffmpeg
止礼9 小时前
FFmpeg8.0.1 Mac环境 CMake本地调试配置
macos·ffmpeg
简鹿视频1 天前
视频转mp4格式具体作步骤
ffmpeg·php·音视频·实时音视频
一点晖光1 天前
centos安装ffmpeg环境
linux·ffmpeg·centos