核心内容摘要
翱翔云端,私享法式浪漫:法国婬乱私人航空6,一次颠覆想象的空中旅程
引言为什么要死磕 FFmpeg你好我是你们的技术老友。
在音视频开发领域流传着这样一句话“音视频开发只有两种一种是基于 FFmpeg 的一种是自己造轮子的。
”FFmpeg 是什么它是音视频领域的瑞士军刀是 VLC、PotPlayer、甚至 YouTube 和 Bilibili 都在使用的底层核心。
但是它的学习曲线之陡峭堪比徒手攀岩。
官方文档晦涩难懂由于历史悠久API 废弃和更迭极快ffmpeg
x,
x,
x,
x 差异巨大。
涉及大量的音视频理论YUV、PCM、PTS/DTS、I/P/B 帧。
C 语言的内存管理稍有不慎就是内存泄漏Memory Leak。
为了帮大家跨过这道坎我耗时一周翻阅了数千行源码整理了这篇万字长文。
不讲虚的只讲干货。
我们将从最基础的音视频原理讲起拆解FFmpeg 架构最后手把手带你用C 写一个视频解码器。
建议先收藏再阅读防止迷路️
分音视频基础知识必修课在碰代码之前如果不懂基础理论你看着 FFmpeg 的 API 就像看天书。
1 视频不仅仅是图片很多新手认为视频就是连续播放的 JPG。
错如果这样存1小时的电影可能需要几百 GB。
视频的核心在于压缩Coding。
帧内压缩Intra-frame类似 JPEG只压缩当前画面去掉人眼不敏感的信息。
帧间压缩Inter-frame这是视频压缩的神技。
比如一个新闻主播在播报背景是不动的只有嘴巴在动。
我们只需要记录变化的部分。
这就引出了视频编码中最重要的概念I 帧、P 帧、B 帧。
I 帧 (Intra Picture)关键帧。
完整画面自给自足不参考别人。
文件最大解码最快。
P 帧 (Predictive)前向预测帧。
参考前面的 I 帧或 P 帧只存差异。
B 帧 (Bi-directional)双向预测帧。
既参考前面也参考后面。
压缩率最高但解码最耗时。
注意由于 B 帧的存在解码顺序和显示顺序是不一样的这导致了 DTS 和 PTS 的区别后文细讲。
2 颜色模型YUV vs RGB屏幕显示用 RGB但视频存储通常用YUV。
Y亮度Luma占用的频带最宽人眼最敏感。
U/V色度Chroma人眼不敏感。
这意味着我们可以疯狂压缩 UV 分量而不影响观感。
常见的YUV4:2:0格式每 4 个 Y 像素共用一组 UV数据量直接砍掉一半。
3 封装格式 vs 编码格式这是最容易混淆的概念。
封装格式 (Container).mp4,.mkv,.avi。
它只是一个盒子用来装视频流、音频流、字幕流。
编码格式 (Codec)H.264 (AVC),H.265 (HEVC),AAC,MP3。
这才是真正压缩数据的算法。
比喻MP4 是一个快递箱H.264 是箱子里的茅台酒。
你不能说“我要喝 MP4”而应该说“我要喝 H.264”。
️
分FFmpeg 核心架构与流程FFmpeg 的强大在于其模块化设计。
要驾驭它你必须熟悉它的“八大金刚”库。
1 核心库介绍库名功能描述备注libavcodec编解码核心库包含所有原生编解码器FFmpeg 的灵魂libavformat封装格式处理负责解封装Demux和封装Mux如打开 MP4 文件libavutil工具库内存管理、数学运算、日志系统libswscale图像转换YUV 转 RGB改变分辨率libswresample音频重采样改变采样率、声道数如
4
1k 转 48klibavfilter滤镜库加水印、去噪、特效处理libavdevice设备输入输出读取摄像头、麦克风、屏幕录制
2 万能的处理流程The Pipeline无论你是做播放器、转码器还是推流器FFmpeg 的生命周期都逃不开下面这张图。
我特意绘制了这张Mermaid 流程图请务必刻在脑子里avformat_open_inputav_read_frameavcodec_send_packetavcodec_receive_frame处理后 AVFrameavcodec_send_frameavcodec_receive_packetav_interleaved_write_frame输入文件 Input File解复用器 Demuxer编码数据包 AVPacket解码器 Decoder原始帧 AVFrame滤镜 Filter Graph处理后 AVFrame编码器 Encoder编码数据包 AVPacket复用器 Muxer输出文件 Output File核心数据结构解析AVFormatContext统领全局包含输入/输出文件的所有信息。
AVCodecContext编解码器的上下文存着宽高、码率等信息。
AVPacket存放编码后的数据如 H.264 的 NALU它是压缩的。
AVFrame存放解码后的数据如 YUV 像素或 PCM 音频它是巨大的。
核心动作Packet(压缩) -Decode-Frame(原始) -Encode-Packet(压缩)
分C 代码实战解码流程Talk is cheap, show me the code.我们将基于FFmpeg
x/
x/
x 通用 API旧版 API 如avcodec_decode_video2已废弃请勿再学编写一个简单的视频解码器将 MP4 视频解码并保存为 YUV 文件。
1 环境准备确保你的开发环境链接了 ffmpeg 的include和lib。
externC{#includelibavcodec/avcodec.h#includelibavformat/avformat.h#includelibswscale/swscale.h#includelibavutil/imgutils.h}#includeiostream
2 核心代码实现为了方便阅读我将代码拆解为几个核心步骤并附带详细注释。
步骤一打开文件与查找流//
分配上下文AVFormatContext*pFormatCtxavformat_alloc_context();//
打开视频文件if(avformat_open_input(pFormatCtx,input.mp4,NULL,NULL)!
{printf(无法打开文件\n);return-1;}//
查找流信息必须步骤否则可能拿不到宽高if(avformat_find_stream_info(pFormatCtx,NULL)
{printf(无法获取流信息\n);return-1;}//
找到视频流的索引intvideoStreamIndex-1;for(inti0;ipFormatCtx-nb_streams;i){if(pFormatCtx-streams[i]-codecpar-codec_typeAVMEDIA_TYPE_VIDEO){videoStreamIndexi;break;}}步骤二配置解码器注意从 FFmpeg
0 开始我们不再直接使用stream-codec而是通过codecpar来复制参数。
//
获取解码器参数AVCodecParameters*pCodecParpFormatCtx-streams[videoStreamIndex]-codecpar;//
查找解码器 (如 H.
constAVCodec*pCodecavcodec_find_decoder(pCodecPar-codec_id);if(!pCodec){printf(找不到解码器\n);return-1;}//
创建解码器上下文AVCodecContext*pCodecCtxavcodec_alloc_context3(pCodec);//
将流参数复制到解码器上下文avcodec_parameters_to_context(pCodecCtx,pCodecPar);//
打开解码器if(avcodec_open2(pCodecCtx,pCodec,NULL)
{printf(无法打开解码器\n);return-1;}步骤三解码循环核心中的核心这里使用新版 APIavcodec_send_packet和avcodec_receive_frame。
这是一种异步的思想你往解码器里塞几个包解码器可能吐出一个帧也可能需要更多包。
AVPacket*pPacketav_packet_alloc();AVFrame*pFrameav_frame_alloc();while(av_read_frame(pFormatCtx,pPacket)
{// 只处理视频流if(pPacket-stream_indexvideoStreamIndex){// --- 发送 Packet 到解码器 ---intretavcodec_send_packet(pCodecCtx,pPacket);if(ret
{printf(发送 Packet 错误\n);break;}// --- 从解码器接收 Frame ---while(ret
{retavcodec_receive_frame(pCodecCtx,pFrame);if(retAVERROR(EAGAIN)||retAVERROR_EOF){// EAGAIN: 需要更多 Packet 才能产出 Frame// EOF: 文件结束break;}elseif(ret
{printf(解码错误\n);break;}// --- 成功拿到一帧画面 (pFrame) ---printf(解码第 %d 帧, 分辨率: %dx%d, 格式: %d\n,pCodecCtx-frame_number,pFrame-width,pFrame-height,pFrame-format);// TODO: 在这里进行 YUV 保存、Swscale 转换或 OpenGL 渲染}}// 重要每次循环结束必须释放 Packet 引用否则内存爆炸av_packet_unref(pPacket);}步骤四资源释放C/C 开发者的自我修养。
av_frame_free(pFrame);av_packet_free(pPacket);avcodec_free_context(pCodecCtx);avformat_close_input(pFormatCtx);⏳
分进阶 - 那些让你头秃的细节如果说上面的代码能让你运行起来那么下面的内容能救你的命。
1 PTS 与 DTS 的爱恨情仇在处理 H.264 视频时你一定会遇到音视频不同步或者画面抖动的问题。
根源通常在PTS (Presentation Time Stamp)和DTS (Decoding Time Stamp)。
DTS告诉解码器什么时候解码这一帧。
PTS告诉播放器什么时候显示这一帧。
对于没有 B 帧的视频PTS DTS。
但是一旦有了B 帧B 帧需要参考后面的 P 帧所以后面的 P 帧必须先解码顺序就变了写入/解码顺序 (DTS): I0, P3, B1, B2显示顺序 (PTS): I0, B1, B2, P3实战坑点如果你手动进行转封装Remuxing必须正确换算 TimeBase。
2 内存对齐 (Memory Alignment)FFmpeg 默认生成的 Frame 数据为了利用 CPU 的 SIMD 指令集加速SSE/AVX通常是内存对齐的。
这意味着pFrame-linesize[0](这一行的跨度)不一定等于pFrame-width。
切记复制 YUV 数据时千万不要直接memcpy(dst, src, width * height)必须逐行复制for(inti0;iheight;i){memcpy(dst_yi*dst_stride,src_yi*src_stride,// src_stride 即 pFrame-linesize[0]width);}❓
分
常见问题 QAQ1: 为什么我的解码器一开始会丢失几帧A: 可能是解码器缓冲区未刷新。
在文件读取结束后需要传入NULLPacket 调用一次avcodec_send_packet进入Draining Mode把解码器里缓存的最后几帧“冲”出来。
Q2: 为什么av_read_frame读出来的包大小都不一样A: 因为视频是变码率VBR压缩的画面复杂时 Packet 大静止时 Packet 小。
Q3: 如何进行硬件解码CUDA/QuickSyncA: 初始化时需要通过av_hwdevice_ctx_create创建硬件设备上下文并将其绑定到AVCodecContext。
流程比软解复杂一倍如果反响热烈我下一篇专门讲硬解
总结与展望看懂了这篇文章你已经迈过了音视频开发最高的门槛。
FFmpeg 的世界浩如烟海我们今天剖析了理论基础I/P/B 帧YUV 格式。
核心流程Demux - Decode - Encode - Mux。
代码实战基于 C 的标准解码模版。
避坑指南PTS/DTS 同步与内存对齐。
掌握了这些你就可以尝试开发自己的播放器、视频剪辑工具甚至是直播推流客户端。
彩蛋互动时间你正在使用 FFmpeg 解决什么具体问题A. 公司项目需要做视频压缩。
B. 自己写播放器练手。
C. 做直播推流RTMP/RTSP。
D. 单纯觉得以前没看懂今天想彻底搞懂。
欢迎在评论区留下你的答案比如选 C我会针对排名前三的需求在下一篇博文中更新对应的《FFmpeg 实战代码库》整理不易如果这篇文章让你对 FFmpeg 有了新的理解请点赞、收藏、关注三连支持一下我们下期见