介绍
音视频开发中,不得不提到FFmpeg, 该开源框架为音视频开发提供了很大帮助, 是音视频开发工程师都应该掌握的工具 FFmpeg 可以用来记录,处理数字音频,视频并将其转换为流的开源工具,采用LPL或GPL许可证, 可以用在Linux服务器,PC(Windows,MacOS), 移动端(Android,iOS)等平台。
编译
从官网下载FFmpeg(ffmpeg.org/download.ht...) 源码,通过configure脚本来实现编译并定制
, 可以在编译前进行裁剪, 同事通过对最终运行到系统以及目标平台的配置来决定对某些模块进行合适的配置, 脚本运行完毕, 会生成 config.mk(makefile) 和 config.h两个文件
bash
./configure --help
主要的参数如下:
- 标准选项: GNU软件例行配置项目,如安装路径 --prefix=...
- 编译,连接选项, 生成静态库还是动态库, 如--disable-static, --enable-shared
- 可执行程序控制选项, 是否生成 FFmpeg, ffplay, ffprobe
- 模块控制选项: 裁剪编译模块
- 能力展示选项, 列出当前源码支持的各种能力集
FFmpeg 总体架构
AVUtil
核心工具库,该模块是最基本的模块之一, 其他模块都会依赖该库做一些基本的音视频处理操作
AVFormat
文件格式和协议库, 封装了Protocol层和Demuxer, Muxer层, 使得协议和格式对于开发者来说是透明的
AVCodec
编解码库, 封装了Codec层, 也可以把其他第三方Codec以插件的形式添加进来,然后为开发者提供统一的接口
AVFilter
音视频滤镜库, 包含音频和视频特效的处理
AVDevice
输入输出设备库, 如果需要播放声音或视频,请打开该模块, 底层使用的是libSDL库
SwrRessample
音频重采样, 对数字音频进行声道数,数据格式,采样率等各种基本信息的转换
SWScale
图像格式的转换, 比如从 YUV转成RGB格式
PostProc
后期处理, AVFilter 依赖该模块的一些基础函数
bit stream filter
bit stream filter 主要应对某些格式的封装转换行为,例如AAC编码有两种常见的封装格式
- 一种是ADTS格式的流,是AAC定义在MPEG2里面的格式
- 另外一种是封装在MPEG4里面的格式,各种格式会在每一帧的前面拼接一个用声道,采样率等信息组成的头, 开发者完全可以手动拼接该头信息,就是把AAC编码器输出的原始码流(ADTS头+ES流)封装进MP4,FLV或MOV格式的容器中
如果想用这个bit stream filter, 需要添加选项
ini
--enable-bsf=aac_adtstoasc
视频中的H264编码,也有两种封装形式
- MP4封装的格式
- 裸的H264格式(一般成为annexb封装格式) FFmpeg 针对视频也提供了对应的bit stream filter,成为H264_mp4toannexb, 可以将mp4封装格式的H264数据包转换为annexb封装格式的
ini
--enable-bsf=h264_mp4toannexb
下面介绍FFmpeg 几个工具的命令行使用方法
ffprobe
javascript
//查看音频文件信息
ffprobe ~/Desktop/test.mp3
Duration: 00:00:21.52, start: 0.000000, bitrate: 320 kb/s
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 320 kb/s
// 查看视频的信息
ffprobe ~/Desktop/test.mp4
ffplay
ffplay 是以ffmpeg 框架为基础, 外加渲染音视频的库libSDL来构建的媒体文件播放器, 所以在安装ffplay之前也要安装对应版本的libSDL作为依赖的组件
使用ffplay 播放一个音频文件
ffplay test.mp3
会弹出一个窗口播放MP3文件
很多开源的播放器都是基于ffplay进行改造的
arduino
ffplay test.mp4 -loop 10 // 循环播放视频10次
ffplay test.mp4 -ast 1 // 播放视频中的第一路音频流
ffplay test.mp4 -vst 1 //播放第一路视频流
ffplay song.pcm -f s16le -channels 2 -ar 44100 // pcm 是裸数据,需要指定格式(-f),声道(channess) 采样率(-ar)才能正确播放, 而 wav 中头部有44字节表示这些数据, 所以不需要提供这三个数据
ffplay -f rawvideo -pixel_format yuv420p -s 480*480 test.yuv // 针对YUV/RGB原始数据,需要高速ffplay格式(-f rawvideo 代表原始格式), 表示格式(-pixel_fromat yuv420p/rgb24),宽高(-s 480*480)
ffplay 音画同步
- 以音频为主时间轴作为同步源
- 以视频为主时间轴作为同步源
- 以外部时钟为主时间轴作为同步源
css
ffplay test.mp4 -sync audio // video, ext(外部时钟)
原理:
- 播放器接收到的
视频帧
或音频帧
, 内部会有时间戳(PTS时钟)来标识它实际应该在什么时候去展示。 - 实际的对齐策略如下:
- 比较视频当前播放时间和音频当前播放时间,如果视频播放过快,则通过加大延迟或重复播放来降低视频播放速度
- 如果视频播放慢了,则通过减少延迟或丢帧来追赶音频播放的时间点
- 关键在于音视频时间的比较以及延迟的计算,当然在比较的过程中会设置一个阈值(Theshold),若超过阈值就进行调整(丢帧渲染或者重复渲染)
ffmpeg
强大的媒体文件转换工具, 可以转换任何格式的媒体文件,并且可以设置AudioFilter以及VideoFilter进行处理和编辑
使用它离线处理音频和视频任何你想做的事情
下面列出它主要的参数
通用参数
-f
fmt: 指定格式(音频或视频格式)-i
filename: 指定输入文件名,在Linux下当然也能指定0.0(屏幕录制
)或摄像头
- -y: 覆盖已有文件
-t
duration: 指定时长-ss
time_off: 从指定的时间开始播放-re
: 代表按照帧率
发送, 尤其在作为推流工具的时候一定要加入该参数, 否则ffmpeg会按照最高速率向流媒体服务器不停地发送数据-map
:指定我输出文件的流映射模式, 例如"-map 1:0 -map 1:1"要求将第二个输入文件的第一个流和第二个流写入输出文件
视频参数
-b
指定比特率(bit/s)播放- -bitexact: 使用标准比特流
- -r rate: 帧速率(fps)
- -s size: 指定分辨率(320*240)
- -aspect: 设置视屏长宽比(4:3, 16:9 或 1.3333, 1.7777)
- -croptop SIZE: 设置顶部切除尺寸(in pixels)
- -cropbottom size: 设置底部切除尺寸
- -cropleft: 左边
- -cropright
- -padtop: 顶部补齐尺寸(pixels)
- -padbottom
- -padleft
- -padright
- -padcolor 补齐带颜色(000000-ffffff)
- -vn: 取消视频的输出
- -vcodec codec: 强制使用codec编解码方式(copy代表不进行重新编码)
音频参数
- -ab: 设置比特率
- -aq: quality/音频质量
- -ar: rate音频采样率(Hz)
- -ac: channels
- -an: 取消音频轨
- -acodec codec: 音频编码(copy不代表音频转码)
- -vol volume: 设置录制音量大小
示例
less
// 列出 ffmpeg 支持的所有格式
ffmpeg -formats
// inputmp4 从第50s开始剪辑20s的时间,输出到文件output.mp4 -ss 指定偏移时间(time offset), -t 指定时长(duration)
ffmpeg -i input.mp4 -ss 00:00:50 -codec copy -t 20 output.mp4
// 提取视频文件的音频文件
ffmpeg -i input.mp4 -vn -acodec copy output.m4a
// 音频静音,只保留视频
ffmpeg -i input.mp4 -an -vcodec copy output.mp4
// 从MP4 文件中抽取视频流导出为裸的H264数据 -an表示不适用音频数据, 视频数据使用mp4toannexb这个bitstream filter(-bsf) 来转换为原始的H264数据
ffmpeg -i output.mp4 -an -vcodec copy -bsf::v h264_mp4toannexb output.h264
// 使用AAC音频数据和H264视频生成 MP4文件, 使用aac_adtstoasc 的 bitstream filter
ffmpeg -i test.aac -i test.h264 -acodec copy -bsf:a aac_adtstoasc -vcodec copy -f mp4 output.mp4
// 对音频文件的编码方式做转换
ffmpeg -i input.wav -acodec libfdk_aac output.aac
// 从WAV音频文件中导出PCM裸数据
ffmpeg -i input.wav -acodec pcm_s16le -f s16le output.pcm
// 重新编码视频文件,复制音频流 同时封装到MP4格式的文件中
ffmpeg -i input.flv -vcodec libx264 -acodec copy output.mp4
// 将一个MP4格式的视频转换成gif格式的动图
ffmpeg -i input.mp4 -vf scale=100:-1 -t 5 -r 10 image.gif
// 将两路声音合并,比如要给一段声音加上背景音乐
ffmpeg -i vocal.wav i- accompany.wav -filter_complex amix=inputs=2:duration=shortest output.wav
// 将一段视频推送到流媒体服务器上
ffmpeg -re -i input.mp4 -acodec copy -f flv rtmp://xxx
// 将流媒体服务器上的流dump到本地
ffmpeg -i http://xxx/xxx.flv -acodec copy -vcodec copy -f flv output.flv
下面介绍FFmpeg API 的使用
首先需要统一下术语, 以及在FFmpeg 中对应的类
AVFormatContext
容器/文件(Container/File): 表示特定格式的多媒体文件, 如MP4,flv,mov等
AVStream
媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据,一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器
一般视频有多路媒体流(视频流,音频流,字幕流)
AVPacket AVFrame
- 数据帧/数据包(Frame/Packet): 通常媒体流是由大量的数据帧组成的, 对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器之中。
AVCodecContext AVCodec
- 编解码器: 以帧为单位实现压缩数据和原始数据之间的相互转换的
引用头文件
arduino
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/pixdesc.h"
注册协议,格式和编解码器
scss
avformat_network_init();
av_register_all();
打开媒体文件源
scss
AVFormatContext *formatContext = avformat_alloc_context();
avformat_open_input(&formatContext, path, NULL, NULL);
avformat_find_stream_info(formatContext, NULL);
可以打开本地磁盘文件, 也可以打开网络媒体资源的一个链接, 链接使用不同的协议, 如RTMP,HTTP等协议的视频源.
寻找各个流,打开对应的解码器
ini
for (int i = 0;i<formatContext->nb_streams;i++) {
AVStream *stream = formatContext->streams[i];
if (AVMEDIA_TYPE_VIDEO == stream->codec->codec_type) { // 视频流
} else if (AVMEDIA_TYPE_AUDIO == stream->codec->codec_type) { // 音频流
}
}
ini
// 打开音频流解码器
AVCodecContext *audioCodecCtx = audioStream->codec;
AVCodec *codec = avcodec_find_decoder(audioCodecCtx->codec_id);
if (!codec) {
// 找不到对应的音频解码器
}
int openCodecErrCode = 0;
if (openCodecErrCode = avcodec_open2(coderCtx, codec, NULL) < 0) {
// 打开音频解码器失败
}
// 打开视频流解码器
AVCodecContext *audioCodecCtx = vidoeStream->codec;
AVCodec *codec = avcodec_find_decoder(audioCodecCtx->codec_id);
if (!codec) {
// 找不到对应的视频解码器
}
int openCodecErrCode = 0;
if (openCodecErrCode = avcodec_open2(coderCtx, codec, NULL) < 0) {
// 打开视频解码器失败
}
初始化解码后数据的结构体
scss
SwrContext *swrContext = NULL;
//4、判断是否需要resampler
if (avCodecContext->sample_fmt != AV_SAMPLE_FMT_S16) {
LOGI("because of audio Codec Is Not Supported so we will init swresampler...");
/**
* 初始化resampler
* @param s Swr context, can be NULL
* @param out_ch_layout output channel layout (AV_CH_LAYOUT_*)
* @param out_sample_fmt output sample format (AV_SAMPLE_FMT_*).
* @param out_sample_rate output sample rate (frequency in Hz)
* @param in_ch_layout input channel layout (AV_CH_LAYOUT_*)
* @param in_sample_fmt input sample format (AV_SAMPLE_FMT_*).
* @param in_sample_rate input sample rate (frequency in Hz)
* @param log_offset logging level offset
* @param log_ctx parent logging context, can be NULL
*/
swrContext = swr_alloc_set_opts(NULL, av_get_default_channel_layout(OUT_PUT_CHANNELS), AV_SAMPLE_FMT_S16, avCodecContext->sample_rate,
av_get_default_channel_layout(avCodecContext->channels), avCodecContext->sample_fmt, avCodecContext->sample_rate, 0, NULL);
if (!swrContext || swr_init(swrContext)) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(avCodecContext);
LOGI("init resampler failed...");
return;
}
pAudioFrame = avcodec_alloc_frame();
}
构建视频的格式转换对象以及视频解码后数据存放的对象
arduino
AVPicture picture;
bool picture\Valid = avpicture_alloc(&picture, PIX_FMT_YUV420P, videoCodecCtx->width, videoCodecCtx->height) == 0;
if (!pictureValid) {
// 分配失败
return false;
}
swsContext = sws_getCachedContext(swsContext, videoCodecCtx->widht, videoCodecCtx->height,videoCodecCtx->pix_fmt,videoCodecCtx->widht,videoCodecCtx->height,PIX_FMT_YUV420P,SWS_FAST_BITLNEAR,NULL,NULL,NULL);
videoFrame = avcodec_alloc_frame();
读取流内容并且解码
ini
AVPacket packet;
int gotFrame = 0;
while(true) {
if (av_read_frame(formatContext, &packet)) {
// End of file
break;
}
int packetStreamIndex = packet.stream_index;
if (packetStreamIndex == videoStreamIndex) {
int len = avcodec_decode_video2(videoCodecCtx, videoFrame, &gotFrame, &packet);
if (len < 0) {
break;
}
if (gotFrame) {
self->handleVideoFrame();
}
} else if (packetStreamIndex == audioStreamIndex) {
int len = avcodec_decode_audio4(audioCodecCtx, audioFrame, &gotFrame, &packet);
if (len < 0) {
break;
}
if (gotFrame) {
self->handleVideoFrame();
}
}
}
FFmpeg 源码结构
libavformat 主要组成和层次调用关系
libavcodec主要组成和数据结构如下图