前言:项目需要增加视频拍摄和压缩的功能,了解到FFmpeg开源并且运用广泛,遂用之
1.1 FFmpeg简单介绍
FFmpeg全名是Fast Forward MPEG(Moving Picture Experts Group),FFmpeg官网。
关于FFmpeg:
FFmpeg是一个全球领先的多媒体框架,能够友好的在大部分设备实现解码、编码、转码、复用、解复用、流媒体、过滤和播放。它支持最不起眼的古老的格式,最高可达前沿。不管他们的一些标准委员会,社区或公司设计。具有较可靠的可移植性:FFmpeg能够在各个平台(Linux、Mac OS X、Microsoft Windows、BSDs、Solaris等)和架构(x86、arm、mips等)中运行和编译,经得起考验。
FFmpeg包含的库:
1.libavcodec
包含全部FFmpeg音频/视频编解码库。
引用:详情点击
libavcodec是一款LGPL自由软件编解码库,用于视频和音频数据的编解码工作。带有这个名字的库有FFmpeg项目和Libav项目,但是它们却彼此不兼容。
libavcodec是个集成了许多开源多媒体应用和框架。常见的MPlayer、xine和VLC媒体播放器都使用它作为它们的主要内置解码引擎,用于许多音频视频格式在所有支持的平台上的重放。它也被ffdshow试用解码器用作主要的解码库。libavcodec也被用于视频编辑和转换应用,例如Avidemux、MEncoder或者Kdenlive既用它解码也用其编码。
libavcodec包含了解码器和为实现若干专有格式而存在的编码器。其本身在逆向工程方面的工作就是libavcodec发展成长的一部分。在标准的libavcodec框架下,有着这些有效的编解码器为使用原始的编解码器带来很大的益处,最明显的就是提升了移植性。另一些方面也增加了它的表现力,因为libavcodec包含了一份标准库,它高度优化了常见创建块的实现,比如DCT和色彩空间转换。但是,即便libavcodec致力于将字节提取级别的解码来实现最终结果,这样的重现中出现的错误和丢失的特征有时候能导致回放固定文件的兼容性问题。
2. libavformat:
实现了流媒体协议(udp、rtp、rtmp、rtsp等),媒体容器(mp4、AVI、Flv等)和基本的I/O访问。
3. libavutil:
是一个实用程序库,以帮助便携式多媒体编程。它包含了安全的移动字符串函数,随机数生成器,数据结构,附加数学功能,加密和多媒体相关的功能(如枚举的像素采样格式)。它并不是libavcodec和libav必备的库。引用:详情点击
4. libavfilter:
是一个通用的音视频后处理库。
5. libavdevice:
提供用于从采集和渲染到许多常见多媒体输入/输出设备的通用框架,并支持多个输入和输出设备,包括Video4Linux2、VfW、DSHOW和ALSA。
6. libswresample:
实现音频的重采样和混音,根据平台做了优化(neon等)。
该libswresample库进行高度优化的音频采样,rematrixing和采样格式转换操作。
重采样:是改变音频速率,例如从44100Hz的高采样率到8000Hz的过程。从高至低采样率的音频转换是一种有损的过程。几个重采样选项和算法是可用的。
格式转换:是将样品的类型,例如从16位有符号的样本为无符号的8位或浮样品的过程。它还处理包装的转换,从包装的布局传递时,以平面布局(属于交织在相同缓冲液不同的信道的所有样品)(属于存储在专用缓冲区或“平面”相同的信道的所有样品)。
Rematrixing:是改变频道布局,例如从立体声到单声道的过程。当输入通道不能被映射到输出数据流,该方法是有损耗的,因为它涉及到不同的增益因子和混合。
其他各种音频转换(如拉伸和填充)通过专用的选项启用。
7.libswscale:
实现了颜色格式的转换和缩放,具有同样功能的另一个库是libyuv,webrtc中采用的是这个,libyuv针对各个平台都做了汇编优化。
FFmpeg提供的工具:
- 【ffmpeg】(ffmpeg官方说明)一个命令行工具,用来对视频文件转换格式,也支持对电视卡实时编码。
- 【ffserver】(ffserver官方说明)一个HTTP多媒体实时广播流服务器,支持时光平移。
- 【ffplay】(ffplay官方说明)一个简单的播放器,基于SDL与FFmpeg库。
- 【ffprobe】(ffprobe官方说明)一个简单的多媒体数据分析工具。
注:官方目前有Linux,Windows,OS X版本,没有Android和iOS版本,本文章使用已移植好的.so文件。
.so点击下载
1.2 对如何编译并移植到Android,感兴趣的可以点击下方
2.0 了解FFmpeg-命令
举例:$ ffmpeg -i input.mp4 output.avi
记住几个重点:
-preset:指定编码的配置。x264编码算法有很多可供配置的参数,不同的参数值会导致编码的速度大相径庭,甚至可能影响质量。为了免去用户了解算法,然后手工配置参数的麻烦。x264提供了一些预设值,而这些预设值可以通过preset指定。这些预设值有包括:ultrafast,superfast,veryfast,faster,fast,medium,slow,slower,veryslow和placebo。ultrafast编码速度最快,但压缩率低,生成的文件更大,placebo则正好相反。x264所取的默认值为medium。需要说明的是,preset主要是影响编码的速度,并不会很大的影响编码出来的结果的质量。压缩高清电影时,我一般用slow或者slower,当你的机器性能很好时也可以使用veryslow,不过一般并不会带来很大的好处。
-crf:这是最重要的一个选项,用于指定输出视频的质量,取值范围是0-51,默认值为23,数字越小输出视频的质量越高。这个选项会直接影响到输出视频的码率。一般来说,压制480p我会用20左右,压制720p我会用16-18,1080p我没尝试过。个人觉得,一般情况下没有必要低于16。最好的办法是大家可以多尝试几个值,每个都压几分钟,看看最后的输出质量和文件大小,自己再按需选择。
注:-crf 可以拆分成自定义 -r (视频帧率) -b:a (音频码率)
以上命令会直接影响到编译速度、视频质量、生成的视频文件的大小
在此不做过多篇幅介绍:FFmpeg命令详细介绍
以下为**-preset **-crf -r -b:a 手动实际测试数据 ****
3.0 .视频文件介绍
介绍此内容是因为在编译时需要写入视频角度,直接影响播放时的画面方向。(需求:手机设备横向拍摄的视频,在手机竖直播放时,播放画面保持横向)
(1)内容元素 ( Content )
--图像 ( Image )
--音频 ( Audio )
--元信息 ( Metadata )
(2)编码格式 ( Codec )
--Video : H.264,H.265, …
--Audio : AAC, HE-AAC, …
(3)容器封装 (Container)
--MP4,MOV,FLV,RM,RMVB,AVI,…
任何一个视频 Video 文件,从结构上讲,都是这样一种组成方式:
由图像和音频构成最基本的内容元素;
图像经过视频编码压缩格式处理(通常是 H.264);
音频经过音频编码压缩格式处理(例如 AAC);
注明相应的元信息(Metadata);
最后经过一遍容器(Container)封装打包(例如 MP4),构成一个完整的视频文件
注:我们需要的视频角度需要写入元信息(Metadata),使用 FFmpeg命令:-metadata:s:v:0 rotate=90
4.0 .代码部分
详细具体看文章末尾的github项目地址,以下只会圈出重点部分 (--填的坑---)
(1)自定义方向传感器监听,需要写入视频角度
/**传感器监听*/
mAlbumOrientationEventListener = new AlbumOrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL);
if (mAlbumOrientationEventListener.canDetectOrientation()) {
mAlbumOrientationEventListener.enable();
} else {
Log.d("TODO", "Can't Detect Orientation");
}
/**
* 自定义方向传感器监听
*/
private class AlbumOrientationEventListener extends OrientationEventListener {
public AlbumOrientationEventListener(Context context) {
super(context);
}
public AlbumOrientationEventListener(Context context, int rate) {
super(context, rate);
}
@Override
public void onOrientationChanged(int orientation) {
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
return;
}
//保证只返回四个方向
int newOrientation = ((orientation + 45) / 90 * 90) % 360;
if (newOrientation != mRotate) {
//返回的mOrientation就是手机方向,为0°、90°、180°和270°中的一个
mRotate = newOrientation;
}
Log.i("TODO","mOrientation="+mRotate);
}
}
(2)视频采集编译
--视频拍摄生成.ts流,拍摄可暂停,生成多个.ts。输出时合成一个.mp4文件
/**
* 合成视频
*/
private void syntVideo(){
//ffmpeg -i concat:ts0.ts|ts1.ts|ts2.ts|ts3.ts -c copy -bsf:a aac_adtstoasc out2.mp4
StringBuilder sb = new StringBuilder("ffmpeg");
sb.append(" -i");
String concat="concat:";
for (MediaObject.MediaPart part : mMediaObject.getMediaParts()){
concat += part.mediaPath;
concat += "|";
}
concat = concat.substring(0, concat.length()-1);
sb.append(" "+concat);
sb.append(" -c");
sb.append(" copy");
if(isAddRotate){
sb.append(" -metadata:s:v:0");
sb.append(" rotate="+mRotate);
}
sb.append(" -bsf:a");
sb.append(" aac_adtstoasc");
sb.append(" -y");
String output = mMediaObject.getOutputVideoPath();
sb.append(" "+output);
int i = UtilityAdapter.FFmpegRun("", sb.toString());
(3)视频自适应屏幕宽度播放
/**自适应屏幕宽度播放视频*/
int videoW = mp.getVideoWidth();
int videoH = mp.getVideoHeight();
int windowW = getWindowManager().getDefaultDisplay().getWidth();
int windowH = getWindowManager().getDefaultDisplay().getHeight();
ViewGroup.LayoutParams layoutParams = vv_play.getLayoutParams();
if(videoW < windowW){
//自适应屏幕宽度
float widthF = windowW/(videoW*1f);
layoutParams.width = getWindowManager().getDefaultDisplay().getWidth();
layoutParams.height = (int) (videoH *widthF);
}else if(videoW == windowW){
//直接显示
layoutParams.width = videoW;
layoutParams.height = videoH;
}else if(videoW > windowW){
//缩小显示
float widthF = videoW*1f/windowW;
layoutParams.width = videoW;
// int newH = (int)(videoH/widthF);
int newH = (int)((windowW/16)*9);
if(newH > windowH){
layoutParams.height = windowH;
}else{
layoutParams.height = newH;
}
}else{
layoutParams.width = videoW;
layoutParams.height = videoH;
}
vv_play.setLayoutParams(layoutParams);
vv_play.setLooping(true);
vv_play.start();
(4)部分机型长宽比为奇数,导致相机预览时黑屏
/**
*计算预览尺寸值 解决bug:部分机型出现 长宽比为奇数,预览黑屏
* */
private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.05;
double targetRatio = (double) w / h;
if (sizes == null) return null;
Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = h;
// Try to find an size match aspect ratio and size
for (Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size; minDiff = Math.abs(size.height - targetHeight);
}
} // Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
(5)视频压缩
/**
* 压缩视频1
* @param inDir 输入目录
* @param outDir 输出目录
* @param videoFramerate 视频帧率
* @param audioFramerate 音频码率
* @return
*/
private Boolean compressVideo(String inDir,String outDir,String videoFramerate,String audioFramerate){
//ffmpeg.exe -i "C:\test.mp4" -r 10 -b:a 32k "C:\test_mod.mp4"
StringBuilder sb = new StringBuilder("ffmpeg");
sb.append(" -i");
sb.append(" "+inDir);
sb.append(" -vcodec libx264");
sb.append(" -preset");
sb.append(" ultrafast");
sb.append(" -r");
sb.append(" "+videoFramerate);
sb.append(" -b:a");
sb.append(" "+audioFramerate);
sb.append(" "+outDir);
int i = UtilityAdapter.FFmpegRun("", sb.toString());
if(i == 0){
//命令执行
return true;
}else{
Toast.makeText(getApplicationContext(), "视频合成失败", Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* 压缩视频2
* @param inDir 输入目录
* @param outDir 输出目录
* @param videoCRF 视频质量
* @return
*/
private Boolean compressVideo(String inDir,String outDir,String videoCRF){
StringBuilder sb = new StringBuilder("ffmpeg");
sb.append(" -i");
sb.append(" "+inDir);
sb.append(" -vcodec libx264");
sb.append(" -preset");
sb.append(" ultrafast");
sb.append(" -crf");
sb.append(" "+videoCRF);
sb.append(" "+outDir);
int i = UtilityAdapter.FFmpegRun("", sb.toString());
if(i == 0){
//命令执行
return true;
}else{
Toast.makeText(getApplicationContext(), "视频合成失败", Toast.LENGTH_SHORT).show();
return false;
}
}
项目预览界面
The end