执行命令 -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() - 写入尾部
📋 分阶段详细调用链
-
程序入口 (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() // 分配调度器
-
参数解析阶段 (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)
// 打开所有输出文件 ↓↓↓
-
输入文件打开 (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 文件头
- 输出文件打开 (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
- 转码主循环 (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)
-
解复用线程 (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)
-
解码流程 (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()
- 编码流程 (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()
- 复用线程 (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数据写入文件
- 清理和退出
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 - 写入输出文件
线程间通过调度器队列传递数据包和帧