FFmpeg-Android拍摄压缩Demo

LOGO.png

前言:项目需要增加视频拍摄和压缩的功能,了解到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提供的工具:

  1. 【ffmpeg】(ffmpeg官方说明)一个命令行工具,用来对视频文件转换格式,也支持对电视卡实时编码。
  2. 【ffserver】(ffserver官方说明)一个HTTP多媒体实时广播流服务器,支持时光平移。
  3. 【ffplay】(ffplay官方说明)一个简单的播放器,基于SDL与FFmpeg库。
  4. 【ffprobe】(ffprobe官方说明)一个简单的多媒体数据分析工具。

注:官方目前有Linux,Windows,OS X版本,没有Android和iOS版本,本文章使用已移植好的.so文件。
.so点击下载

1.2 对如何编译并移植到Android,感兴趣的可以点击下方

Android Studio编译FFmpeg库

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 手动实际测试数据 ****

测试数据.png

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;
        }
    }

项目预览界面

1502259331(1).png

APK点击下载

github项目链接

The end

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容