1. 学习大纲
FFmpeg 常用命令:
- 视频录制命令
- 多媒体文件的分解/复用命令
- 裁剪与合并命令
- 图片/视频互转命令
- 直播相关命令
- 各种滤镜命令
FFmpeg 基本开发:
- C 语言回顾
- FFmpeg 核心概念与常用结构体
- 实战 - 多媒体文件的分解与复用
- 实战 - 多媒体格式的互转
- 实战 - 从 MP4 裁剪一段视频
- 作业 - 实现一个简单的小咖秀
音视频编解码实战:
- 实战 - H264 解码
- 实战 - H264 编码
- 实战 - 音频 AAC 解码
- 实战 - 音频 AAC 编码
- 实战 - 视频转图片
音视频渲染实战:
- SDL 事件处理
- SDL 视频文理渲染
- SDL 音频渲染
- 实战1 - 实现 YUV 视频播放
- 实战2 - YUV 视频倍数播放
- 实战3 - 实现 PCM 播放器
FFmpeg 开发播放器核心功能:
- 实战 - 实现 MP4 文件的视频播放
- 实战 - 实现 MP4 文件的音频播放
- 实战 - 实现一个初级播放器
- 实战 - 音视频同步
- 实战 - 实现播放器内核
Android 中实战 FFmpeg:
- 编译 Android 端可以使用的 FFmpeg
- Java 与 C 语言相互调用
- 实战 - Android 调用 FFmpeg
学习建议:
- 牢牢抓住音视频的处理机制,了解其本质
- 勤加练习,熟能生巧
- 待着问题去学习,事半功倍
音视频的广泛应用:
- 直播类:音视频会议、教育直播、娱乐/游戏直播
- 短视频:抖音、快手、小咖秀
- 网络视频:优酷、腾讯视频、爱奇艺等
- 音视频通话:微信、QQ、Skype等
- 视频监控
- 人工智能:人脸识别,智能音箱等,更关注算法
播放器架构:
渲染流程:
FFmpeg 都能做啥:
- FFmpeg 是一个非常优秀的多媒体框架
- FFmpeg 可以运行在 Linux、Mac、Windows 等平台上
- 能够解码、编码、转码、复用、解复用、过滤音视频数据
FFmpeg 下载与安装:
FFMpeg 下载与安装
1 | git clone https://git.ffmpeg.org/ffmpeg.git |
2. FFmpeg 常用命令实战
我们按使用目的可以将 FFMPEG 命令分成以下几类:
- 基本信息查询命令
- 录制
- 分解 / 复用
- 处理原始数据
- 滤镜
- 切割与合并
- 图/视互转
- 直播相关
除了 FFMPEG 的基本信息查询命令外,其它命令都按下图所示的流程处理音视频。
1 | ffplay -s 2560x1600 -pix_fmt uyvy422 out.yuv |
3. 初级开发内容
- FFmpeg 日志的使用及目录的操作
- 介绍 FFmpeg 的基本概念及常用的结构体
- 对复用/解复用及流程操作的各种实践
FFmpeg 代码结构:
- libavcodec: 提供了一系列编码器的实现。
- libavformat: 实现在流协议,容器格式及其本IO访问。
- libavutil: 包括了hash器,解码器和各类工具函数。
- libavfilter: 提供了各种音视频过滤器。
- libavdevice: 提供了访问捕获设备和回放设备的接口。
- libswresample: 实现了混音和重采样。
- libswscale: 实现了色彩转换和缩放工能。
3.1 FFmpeg 日志系统
1 |
|
- AV_LOG_ERROR
- AV_LOG_WARNING
- AV_LOG_INFO
FFmpeg日志系统使用
1 |
|
3.2 FFmpeg 文件与目录操作
文件的删除与重命名:
1 |
|
FFmpeg文件与目录操作
1 |
|
1 | clang -g -o ffmpeg_del ffmpeg_file.c `pkg-config --libs libavformat` |
3.3 FFmpeg 操作目录重要函数
1 | avio_open_dir() |
操作目录重要结构体:
AVIODirContext
操作目录的上下文
AVIODirEntry
目录项。用于存放文件名,文件大小等信息
FFmpeg操作目录
1 |
|
1 | clang -g -o list ffmpeg_list.c `pkg-config --libs libavformat libavutil` |
3.4 多媒体文件的基本概念
- 多媒体文件其实是个容器
- 在容器里有很多流(Stream/Track)
- 每种流是由不同的编码器编码的
- 从流中读出的数据称为包
- 在一个包中包含着一个或多个帧
几个重要的结构体:
- AVFormatContext
- AVStream
- AVPacket
FFmpeg 操作流数据的基本步骤:
解复用 —> 获取流 —> 读取数据包 —> 释放资源
3.5 [实战] 打印音/视频信息
1 | av_register_all() |
[实战] 打印音/视频信息
1 |
|
3.6 [实战] 抽取音频数据
1 | av_init_packet() |
[实战] 抽取音频数据
1 |
|
1 | lang -g -o extra_audio extra_audio.c `pkg-config --libs libavutil libavformat` |
3.7 [实战] 抽取视频数据
- Start code
- SPS/PPS
- codec -> extradata
3.8 [实战] 将 MP4 转成 FLV 格式
1 | avformat_alloc_output_context2() / avformat_free_context(); |
3.9 [实战] 从 MP4 截取一段视频
1 | av_seek_frame() |
从 MP4 截取一段视频代码:
1 |
|
3.10 [实战] 一个简单的小咖秀
- 将两个媒体文件中分别抽取音频与视频轨
- 将音频与视频轨合并成一个新文件
对音频与视频轨进行裁剪
4. FFmpeg 中级开发内容
FFmpeg H264 解码
- FFmpeg H264 编码
- FFmpeg AAC 解码
- FFmpeg AAC 编码
4.1 FFmpeg H264 解码
1 |
常用数据结构:
- AVCodec 编码器结构体
- AVCodecContext 编码器上下文
AVFrame 解码后的帧
结构体内存的分配与释放:
1 | av_frame_alloc / av_frame_free(); |
解码步骤:
- 查找解码器(avcodec_find_decoder)
- 打开解码器(avcodec_open2)
- 解码(avcodec_decode_video2)
4.2 FFmpeg H264 编码
H264编码流程:
- 查找编码器(avcodec_find_encoder_by_name)
- 设置参数,打开编码器(avcondec_open2)
- 编码(avcondec_encode_video2)
4.3 视频转图片
TODO
4.4 FFmpeg AAC 编码
- 编码流程与视频相同
- 编码函数 avcodec_encodec_audio2
5. SDL 介绍
语法与子系统:
SDL将功能分成下列数个子系统(subsystem):
- Video(图像)—图像控制以及线程(thread)和事件管理(event)。
- Audio(声音)—声音控制
- Joystick(摇杆)—游戏摇杆控制
- CD-ROM(光盘驱动器)—光盘媒体控制
- Window Management(视窗管理)-与视窗程序设计集成
- Event(事件驱动)-处理事件驱动
以下是一支用C语言写成、非常简单的SDL示例:
1 | // Headers |
上述程序会加载所有SDL子系统(出错则退出程序),然后暂停两秒,最后关闭SDL并退出程序。
5.1 SDL 编译与安装
- 下载 SDL 源码
- 生成Makefile configure –prefix=/usr/local
- 安装 sudo make -j 8 && make install
5.2 使用 SDL 基本步骤
- 添加头文件 #include <SDL.h>
- 初始化 SDL
- 退出 SDL
SDL 渲染窗口:
1 | SDL_Init() / SDL_Quit(); |
1 | clang -g -o first_sdl first_sdl.c `pkg-config --libs sdl2` |
SDL 渲染窗口:
1 | SDL_CreateRender() / SDL_DestoryRenderer(); |
5.3 SDL 事件基本原理
- SDL 将所有的事件都存放在一个队列中
- 所有对事件的操作,其实就是队列的操作
SDL 事件种类:
- SDL_WindowEvent:窗口事件
- SDL_KeyboardEvent:键盘事件
- SDL_MouseMotionEvent:鼠标事件
- 自定义事件
SDL 事件处理:
1 | SDL_PollEvent(); // 轮询检测 |
5.4 文理渲染
SDL 渲染基本原理:
SDL 文理相关 API:
1 | SDL_CreateTexture(); |
SDL 渲染相关 API:
1 | SDL_SetRenderTarget(); |
5.5 [实战] YUV 视频播放器
创建线程:
1 | SDL_CreateThread(); |
SDL 更新文理:
1 | SDL_UpdateTexture(); |
5.6 SDL 播放音频
播放音频基本流程:
播放音频的基本原则:
- 声卡向你要数据而不是你主动推给声卡
- 数据的多少由音频参数决定的
SDL 音频 API:
1 | SDL_OpenAudio() / SDL_CloseAudio(); |
5.7 实现 PCM 播放器
TODO
6. 最简单的播放器
- 该播放器只实现视频播放
- 将 FFmpeg 与 SDL 结合到一起
- 通过 FFmpeg 解码视频数据
- 通过 SDL 进行渲染
1 | clang -g -o player2 player2.c `pkg-config --cflags --libs sdl2 libavformat libavutil libswscale libavcodec libswresample` |
最简单的播放器之二:
- 可以同时播放音频与视频
- 使用队列存放音频包
6.1 多线程与锁
为什么要用多线程:
- 多线程的好处
- 多线程带来的问题
线程的互斥与同步:
互斥
同步
大的任务分为很多小任务通过信号协调
锁与信号量:
- 锁的种类
- 通过信号进行同步
锁的中种类:
- 读写锁
- 自旋锁
- 可重入锁
SDL 线程的创建:
1 | SDL_CreateThread(); |
SDL 锁:
1 | SDL_CreateMutex() / SDL_DestroyMutex(); // 创建互斥量 |
SDL 条件变量:
1 | SDL_CreateCond() / SDL_DestroyCond(); |
6.2 锁与条件变量的使用
TODO
6.3 播放器线程模型
6.4 线程的退出机制
- 主线程接收到退出事件
- 解复用线程在循环分流时对 quit 进行判断
- 视频解码线程从视频流队列中取包时对 quit 进行判断
- 音视解码从音频流队列中取包时对 quit 进行判断
- 音视循环解码时对 quit 进行判断
- 在收到信号变量消息时对 quit 进行判断
6.5 音视频同步
时间戳:
- PTS:Presentation timestamp 渲染时间戳
- DTS:Decoding timestamp 解码时间戳
- I(intra)/ B(bidirectional)/ P(predicted)帧
时间戳顺序:
- 实际帧顺序:I B B P
- 存放帧顺序:I P B B
- 解码时间戳:1 4 2 3
- 展示时间戳:1 2 3 4
由于有了 B 帧之后,它打乱了 PTS 时间戳,所以加了 DTS 解码时间戳。在大多数没有 B 帧的情况下 PTS 和 DTS 是一致的。
从哪儿获得 PTS:
- AVPacket 中的 PTS
- AVFrame 中的 PTS
- av_frame_get_best_effort_timestamp()
时间基:
- tbr:帧率
- tbn:time base of stream 流的时间基
- tbc:time base of codec 解码的时间基
计算当前帧的 PTS:
PTS = PTS * av_q2d(video_stream->time_base)
av_q2d(AVRotional a){ return a.num / (double)a.den; }
计算下一帧的 PTS:
- video_clock:预测的下一帧视频的 PTS
- frame_delay:1/tbr
- audio_clock:音频当前播放的时间戳
音视频同步的时候需要计算 audio_clock 和 video_clock,看视屏时间是在音频时间之前还是在音频时间之后,如果是在音频时间之前就立即播放,如果在音频时间之后需要 delay 一段时间播放(delay的时间计算:audio_clock - video_clock)
音视频同步方式:
- 视频同步到音频
- 音频同步到视频
- 音频和视频都同步到系统时钟
视频播放的基本思路:
- 一般的做法,展示第一帧视频帧后,获得要显示的下一个视频帧的 PTS,然后设置一个定时器,当定时器超时时后,刷新新的视屏帧,如此反复操作。
最简单的播放器:
1 |
|
7. 如何在 Android 下使用 FFmpeg
Android 架构:
内容:
- Java 与 C 之间的相互调用
- Android 下 FFmpeg 的编译
- Android 下如何使用FFmpeg
第一个 JNI 程序:
TODO
JNI 基本概念:
- JNIEnv
- JavaVM 一个Android APP只有一个 JavaVM, 一个 JavaVM 可以有多个JNIEnv
- 线程 一个线程对应一个JNIEnv
Java调用C/C++ 方法一:
在Java层定义 native 关键字函数
方法一:在C/C++层创建
Java_packname_classname_methodname 函数
Java调用C/C++方法二:
什么是Signature:
- Java与C/C++ 相互调用时,表式函数参数的描述符
- 输入参数放在()内,输出参数放在()外
- 多个参数之间顺序存放,且用 “;” 分割
C/C++ 调用 Java 方法:
- FindClass
- GetMethodID / GetFieldID
- NewObject
Call<TYPE>Method / [G/S]et<type>Field
7.1 [实战] Android 下的播放器
TODO
8. IOS 下使用 FFmpeg
TODO
9. 音视频进阶
- FFmpeg Filter 的使用
- FFmpeg 裁剪与优化
- 视频渲染(OpenGL / Metal)
- 声音的特效
- 网络传输
- Webrtc - 实时互动、直播、P2P音视频传输
- AR技术
- OpenCV
行业痛点:
- 回音消除
- 降噪
- 视频秒开
- 多人多视频实时互动
- PC端/APP/网页实时视频互通
- 实时互动与大并发负载
FFmpeg音视频同步原理与实现
音视频同步原理
如果简单的按照音频的采样率与视频的帧率去播放,由于机器运行速度,解码效率等种种造成时间差异的因素影响,很难同步,音视频时间差将会呈现线性增长。所以要做音视频的同步,有三种方式:
参考一个外部时钟,将音频与视频同步至此时间。我首先想到这种方式,但是并不好,由于某些生物学的原理,人对声音的变化比较敏感,但是对视觉变化不太敏感。所以频繁的去调整声音的播放会有些刺耳或者杂音吧影响用户体验。(ps:顺便科普生物学知识,自我感觉好高大上_)。
- 以视频为基准,音频去同步视频的时间。不采用,理由同上。
- 以音频为基准,视频去同步音频的时间。 所以这个办法了。
所以,原理就是以音频时间为基准,判断视频快了还是慢了,从而调整视频速度。其实是一个动态的追赶与等待的过程。
一些概念
音视频中都有 DTS
与 PTS
。
- DTS ,Decoding Time Stamp,解码时间戳,告诉解码器packet的解码顺序。
- PTS ,Presentation Time Stamp,显示时间戳,指示从packet中解码出来的数据的显示顺序。
- 音频中二者是相同的,但是视频由于B帧(双向预测)的存在,会造成解码顺序与显示顺序并不相同,也就是视频中 DTS 与 PTS 不一定相同。
时间基 : 看 FFmpeg 源码
1 | AVRational time_base; |
个人理解,其实就是 ffmpeg中 的用分数表示时间单位,num 为分子,den 为分母。并且 ffmpeg 提供了计算方法:
1 | /** |
所以 视频中某帧的显示时间 计算方式为(单位为妙):
1 | time = pts * av_q2d(time_base); |
同步代码
音频部分
clock 为音频的播放时长(从开始到当前的时间)
1 | if (packet->pts != AV_NOPTS_VALUE) { |
然后加上此 packet 中数据需要播放的时间
1 | double time = datalen/((double) 44100 *2 * 2); |
datalen 为数据长度。采样率为 44100,采样位数为 16,通道数为 2。所以 数据长度 / 每秒字节数。
ps:此处计算方式不是很完美,有很多问题,回头研究在再补上。
视频部分
先定义几个值:
1 | double last_play //上一帧的播放时间 |
FFmpeg 痛点解决
回音消除解决方案:
视频秒开:
多人视频实时互动:
实时互动与大并发负载: