从开发小白到音视频专家 七牛云
本文在 Mac 系统下操作
安装
下载
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
配置
./configure --prefix=/usr/local/Cellar/ffmpeg --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --enable-debug --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags= --host-ldflags=
报错进行安装
Q: nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.
A: brew install yasm
Q:ERROR: libfdk_aac not found
A: brew install fdk-aac
编译安装
make && make install
Q:mkdir: /user/local/ffmpeg/lib: Permission denied
make: *** [install-libavdevice-static] Error 1
A: 修改--prefix=/usr/local/Cellar/ffmpeg
路径,之前是/usr/local/ffmpeg
Q: speex not found using pkg-config / x265 not found using pkg-config
A1: 强制指定
./configure --prefix=/usr/local/Cellar/ffmpeg --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --enable-debug --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags= --host-ldflags=\
--extra-ldflags="-L/usr/local/Cellar/speex/1.2.0/lib -L/usr/local/Cellar/x265/3.0/lib" \
--extra-cflags="-I/usr/local/Cellar/speex/1.2.0/include -I/usr/local/Cellar/x265/3.0/include"
亲测结果为:speex强制指定有效,x265无效仍报错
A2: 如果没有安装x265,先安装brew install x265
;如果已经安装,pkg-config --list-all
查看所有关联的包是否有x265,如果没有重新安装x265 brew reinstall x265
。检查一下环境变量的配置 PKG_CONFIG_PATH
编译
gcc/clang -g -O2 -o test test.c -I ... -L ... -lxxx
-g:输出文件中的调试信息
-O:对输出文件做指令优化,-O2编译器优化,-O1不优化
-o:输出文件
-I:指定头文件位置
-L:指定库文件的位置
-l:指定使用哪个库
编译
vi add.h // 头文件声明
#ifndef __MY_LIBRARY__
#define __MY_LIBRARY__
int add(int a, int b);
#endif
vi add.c
#ifndef __MY_LIBRARY__
#define __MY_LIBRARY__
int add(int a, int b){
return (a+b);
}
#endif //__MY_LIBRARAY__
clang -g -c add.c // -c 编译生成 add.o
libtool -static -o libmylib.a add.o // 输出生成libmylib.a第三方库
vi test_lib.c
#include // 尖括号指定位置
#include "add.h" //引入第三方库,双引号,优先在本地目录搜索
int main(int argc, char* argv[])
{
printf("add=%d\n",add(1,3));
return 0;
}
clang -g -o test_lib test_lib.c -I . -L . -lmylib
./test_lib // 执行程序
调试
命令 | gdb | lldb |
---|---|---|
设置断点 breakpoint | b | b |
运行程序 run | r | r |
单步执行 next | n | n |
跳入函数 step | s | s |
挑出函数 | finish | finish |
打印内容print | p | p |
继续执行完 continue | c | c |
查看断点 | break list | break list |
退出 | quit | quit |
查看指针 | x/6d(s) xxxx | x/6d(s) xxxx |
test_lib.dSYM/Contents/Resources/DWARF
调试信息:指令地址、对应源代码及行号
dwarfdump test_lib
代码结构
库 | 描述 |
---|---|
libavcodec | 编码器的实现 |
libavformate | 实现在流协议,容器格式及其本 IO 访问 |
libavutil | 包括了 hash 器,解码器和各种工具函数 |
libavfilter | 提供各种音视频过滤器 |
libavdevice | 提供了访问捕获设备和回访设备的接口 |
libswresample | 实现了混音和重采样 |
libswscale | 实现了色彩转换和缩放功能 |
日志系统
步骤
include
ac_log_set_level(AV_LOG_DEBUG) 设置阈值
av_log(NULL, AV_LOG_INFO, "%s \n", op)
日志级别
AV_LOG_ERROR
AV_LOG_WARNING
AV_LOG_INFO
AV_LOG_DEBUG
文件的删除和重命名
avpriv_io_delete()
avpriv_io_move(src, dst)
// 设置 ffmpeg的 pkgconfig的环境变量 PKG_CONFIG_PATH
// export PKG_CONFIG_PATH=/usr/local/Cellar/ffmpeg/lib/pkgconfig 命令行单独使用只本次使用有效
pkg-config --cflags --libs libavformat
-I/usr/local/Cellar/ffmpeg/include -L/usr/local/Cellar/ffmpeg/lib -lavformat
clang -g -o ffmpeg_del ffmpeg_file.c `pkg-config --cflags --libs libavformat`
操作目录重要函数
avio_open_dir()
avio_read_dir()
avio_close_dir()
AVIODirContext
操作目录的上下文
AVIODirEntry
目录项,用于存放文件名,文件大小等属性信息
#include
#include
#include
int main(int argc,char* argv[])
{
int ret;
AVIODirContext *ctx = NULL;
AVIODirEntry *entry = NULL;
av_log_set_level(AV_LOG_INFO);
ret = avio_open_dir(&ctx, "./", NULL);
if(ret < 0){
av_log(NULL, AV_LOG_ERROR,"Cant open dir: %s\n", av_err2str(ret));
goto __fail;
}
while(1){
ret = avio_read_dir(ctx, &entry);
if(ret < 0){
av_log(NULL, AV_LOG_ERROR, "Cant read dir: %s \n", av_err2str(ret));
goto __fail;
}
if(!entry){
break;
}
av_log(NULL,AV_LOG_INFO, "%12"PRId64" %s \n", entry->size, entry -> name);
avio_free_directory_entry(&entry);
}
__fail:
avio_close_dir(&ctx);
return 0;
}
多媒体文件的基本概念
多媒体文件其实是个容器
在容器里有很多流/轨(Stream/Track),不交叉
每种流是由不同的编码器编码的
从流中读出的数据称为包
在一个包中包含着一个或多个帧
几个重要的结构体
AVFormatContext
AVStream
AVPacket
操作流数据的基本步骤
解复用 -> 获取流 -> 读数据包 ->释放资源
实战打印音视频信息
av_register_all()
avformat_open_input()/avformat_close_input()
av_dump_format()
#include
#include
int main(int argc, char* argv[])
{
AVFormatContext *fmt_ctx = NULL;
int ret = 0;
av_log_set_level(AV_LOG_INFO);
av_register_all();
ret = avformat_open_input(&fmt_ctx, "./test.mp4", NULL, NULL);
if(ret < 0){
av_log(NULL, AV_LOG_ERROR, "Can't open file: %s \n", av_err2str(ret));
return -1;
}
av_dump_format(fmt_ctx, 0, "./test.mp4", 0); // 第四个参数 0/1 输入/输出
avformat_close_input(&fmt_ctx);
return 0;
}
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from './test.mp4':
Metadata:
major_brand : mp42
minor_version : 512
compatible_brands: isomiso2avc1mp41
creation_time : 2019-04-16T01:08:00.000000Z
Duration: 00:00:15.30, bitrate: N/A
Stream #0:0(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, 2 channels, 2 kb/s (default)
Metadata:
creation_time : 2019-04-16T01:08:00.000000Z
Stream #0:1(und): Video: h264 (avc1 / 0x31637661), none(bt709), 1920x1080, 5736 kb/s, 30 fps, 30 tbr, 3k tbn (default)
Metadata:
creation_time : 2019-04-16T01:08:00.000000Z
encoder : JVT/AVC Coding
实战抽取音频数据
- av_init_packet()
- av_find_best_stream()
- av_read_frame() / av_packet_unref()admin
#include
#include
#include
int main(int argc, char* argv[])
{
int ret;
int audio_index;
int len;
char* src = NULL;
char* dst = NULL;
AVPacket pkt;
AVFormatContext *fmt_ctx = NULL;
// 日志级别
av_log_set_level(AV_LOG_INFO);
// 注册所有的编解码器和协议 register all format and codec
av_register_all();
// 1. read two params from console
if(argc < 3){
av_log(NULL, AV_LOG_ERROR, "the count of params should be more that three!\n");
return -1;
}
src = argv[1];
dst = argv[2];
if(!src || !dst){
av_log(NULL, AV_LOG_ERROR, "src or dst is null!\n");
return -1;
}
ret = avformat_open_input(&fmt_ctx, src, NULL, NULL);
if(ret < 0){
av_log(NULL, AV_LOG_ERROR, "Can't open file: %s \n", av_err2str(ret));
return -1;
}
// 输出 meta 信息
av_dump_format(fmt_ctx, 0, src, 0);
// 创建输出文件
FILE* dst_fd = fopen(dst, "wb");
if(!dst_fd){
av_log(NULL, AV_LOG_ERROR, "Can't open out file!\n");
avformat_close_input(&fmt_ctx);
return -1;
}
// 2. get Stream
ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if(ret < 0){
av_log(NULL, AV_LOG_ERROR, "Cant't find the best stream!\n");
avformat_close_input(&fmt_ctx);
fclose(dst_fd);
return -1;
}
audio_index = ret;
// 初始化包
av_init_packet(&pkt);
// 获取每个包
while(av_read_frame(fmt_ctx, &pkt) >= 0){
if(pkt.stream_index == audio_index){
// 3. write audio data to aac file
len = fwrite(pkt.data, 1, pkt.size, dst_fd);
if(len != pkt.size){
av_log(NULL, AV_LOG_WARNING, "waning, length of data is not equal size of pkt!\n");
}
}
av_packet_unref(&pkt);
}
avformat_close_input(&fmt_ctx);
if(dst_fd){
fclose(dst_fd);
}
return 0;
}
抽取视频数据
- Start code 特征码
区分视频帧
开始 + 视频帧的长度
开始 + 特征码
- SPS / PPS
作用:解码视频参数,例如视频帧分辨率、帧率
参数放在哪?
一般需要一个 SPS / PPS,每次解码都有相同的
当分辨率发生变化时,需要更新 SPS / PPS,一般有多个。
直播流里,网络的原因丢数据,切换分辨率丢帧,在每一帧添加 SPS / PPS,不会增加流量,只有几个字节很小,没有任何负担
切换分辨率等变换 SPS / PPS,数据丢包在每一帧添加 SPS / PPS
- codec -> extradata
从编码器 codec 的扩展数据 extradata 中##获取 SPS / PPS##,不是在正常的存储数据包,
将 MP4 转成 FLV 格式
- avformat_alloc_output_context2() / avformat_free_context()
- avformat_new_stream()
- avcodec_parameters_copy()
- avformat_write_header()
- av_write_frame() / av_interleaved_write_frame()
- av_write_trailer()
从 MP4 截取一段视频
- av_seek_frame()
FFmpeg 中级开发
H264解码
H264编码
AAC 解码
AAC 编码
FFmpeg H264解码
添加头文件
- libavcodec / avcodec.h
常用数据结构
- AVCodec 编码器结构体
- AVCodecContext 编码器上下文
- AVFrame 解码后的帧
结构体内存的分配与释放
- av_frame_alloc() / av_frame_free()
- avcodec_alloc_context3()
- avcodec_free_context()
解码步骤
- 查找解码器(avcodec_find_decoder)
- 打开解码器(avcodec_open2)
- 解码(avcodec_decode_video2)
FFmpeg H264编码
H264编码流程
- 查找编码器(avcodec_find_encoder_by_name) 解码通过 id,编码通过 name
- 设置编码参数,并打开编码器(avcodec_open2)
- 编码(avcodec_encode_video2)
SDL
介绍
- SDL:Simple DirectMedia Layer
- C语言实现的跨平台的媒体开源库
- 多用于开发游戏/模拟器/媒体播放器等多媒体应用领域
SDL编译与安装
- 下载SDL的源码 http://www.libsdl.org/
- 生成Makefile,
./configure --prefix=/usr/local/Cell
- 安装
sudo make -j 8 && make install
SDL使用步骤
- 添加头文件 #include
- 初始化SDL
- 退出SDL
SDL渲染窗口
- SDL_Init/SDL_Quit()
- SDL_CreateWindow() / SDL_DestroyWindow()
- SDL_CreateRender() / SDL_DestroyRender()
- SDL_RenderClear()
- SDL_RenderPresent
SDL 事件基本原理
- SDL将所有的事件都存放在一个队列中
- 所有对事件的操作,其实就是对队列的操作
SDL事件种类
- SDL_WindowEvent:窗口事件
- SDL_KeyboardEvent:键盘事件
- SDL_MouseMotionEvent:鼠标事件
SDL事件处理
- SDL_PollEvent
- SDL_WaitEvent
- SDL_WaitEventTimeout
纹理渲染
纹理
SDL渲染基本原理
SDL纹理相关的API
- SDL_CreateTexture()
format:YUV, RGB
access: Texture类型,Target,Stream - SDL_DestroyTexture()
SDL渲染相关API
- SDL_SetRenderTarget()
- SDL_RenderClear()
- SDL_RenderCopy()
- SDL_RenderPresent()
YUV 视频播放器
创建线程
- SDL_CreateThread()
fn: 线程执行函数
name: 线程名
data: 执行函数参数
SDL_更新纹理
- SDL_UpdateTexture()
- SDL_UpdateYUVTexture() 效率更高
SDL播放音频
播放音频基本流程
播放音频的基本原则
- 声卡向你要数据而不是你主动推给声卡
- 数据的多少由音频参数决定的
SDL 音频 API
- SDL_OpenAudio/SDL_CloseAudio
- SDL_PauseAudio
- SDL_MixAudio 混音 API
最简单的播放器
- 该播放器只实现视频播放
- 将 FFmpeg 与 SDL 结合到一起
- 通过 FFmpeg 解码视频数据
- 通过 SDL 渲染
多线程与锁
为啥要用多线程
- 多线程的好处,管理充分利用 CPU
- 多线程带来的问题
线程的互斥与同步
- 互斥
- 同步
锁与信号量
- 锁的种类
- 读写锁
- 自旋锁
- 可重入锁
- 通过信号进行同步
SDL 线程的创建
- SDL_CreateThread
- SDL_WaitThread
SDL锁
- SDL_CreateMutex / SDL_DestroyMutex
- SDL_LockMutex / SDL_UnlockMutex
SDL条件变量
- SDL_CreateCond / SDL_DestroyCond
- SDL_CondWait / SDL_CondSignal
播放器线程模型
音视频同步
时间戳
- 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
从哪获得 PTS
- AVPacket 中的 PTS
- AVFrame 中的 PTS
- av_frame_get_best_effort_timestamp()
时间基
- tbs:time base rate 帧率
- 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: 音频当前播放的时间戳
音视频同步方式
- 视频同步到音频
- 音频同步到视频
- 音频和视频都同步到系统时钟
视频播放的基本思路
一般的做法,展示第一针视频帧之后,获取要显示的下一视频帧的 PTS,然后设置一个定时器,当定时器超时后,刷新新的视频帧,如此反复操作。