Android通过FFmpeg实现多段小视频合成

前面的文章可以说已经入门,后面主要研究ffmpeg的深入功能以及对常用功能,这里主要实现一下多视频合成,主要困难是手机前置摄像头和后置摄像头录制的视频合成问题,我这里主要实现了功能,但是效率不优,暂时记录一下,如果有更好的方式再更新。

如果本文的内容没有看明白,请先查看之前的文章
Android Studio下编译FFmpeg so文件
Android通过FFmpeg实现小视频音频以及背景音乐合成

1.新建SelectRecordActivity类,并且打开AndroidManifest.xml修改为启动类(之前的启动类是MainActivity,现在只是作为一个单独的功能类),引用activity_select_record.xml布局文件,两个选择按钮。
image.png

点击单段视频录制直接跳转到MainActivity,其中的功能就是上面第二篇文章的内容,本文主要实现第二按钮的功能,点击多段视频录制合成跳转到MultiRecordActivity

2.新建MultiRecordActivity,直接引用MainActivity的布局activity_main.xml。因为布局都一样,主要区别在于修改切换摄像头的按钮逻辑以及停止录制的逻辑。
image.png

需要用到变量

/**
     * 相机预览
     */
    private SurfaceView mSurfaceView;
    /**
     * 开始录制按钮
     */
    private ImageView mStartVideo;
    /**
     * 正在录制按钮,再次点击,停止录制
     */
    private ImageView mStartVideoIng;
    /**
     * 录制时间
     */
    private TextView mTime;
    /**
     * 录制进度条
     */
    private ProgressBar mProgress;
    /**
     * 等待视频合成完成提示
     */
    private ProgressBar mWait;
    /**
     * 录制主要工具类
     */
    private MediaHelper mMediaHelper;
    /**
     * 录制进度值
     */
    private int mProgressNumber=0;
    /**
     * 视频段文件编号
     */
    private int mVideoNumber=1;
    private FileUtils mFileUtils;
    /**
     * 临时记录每段视频的参数内容
     */
    private List<Mp4TsVideo> mTsVideo = new ArrayList<>();
    /**
     * mp4转ts流后的地址,主要合成的文件
     */
    private List<String> mTsPath = new ArrayList<>();
    /**
     * 是否已经取消下一步,比如关闭了页面,就不再做线程处理,结束任务
     */
    private boolean isCancel;
    /**
     * 权限相关
     */
    private PermissionHelper mPermissionHelper;

初始化录制工具类以及文件类

mMediaHelper = new MediaHelper(this);
        mMediaHelper.setTargetDir(new File(mFileUtils.getMediaVideoPath()));
        //视频段从编号1开始
        mMediaHelper.setTargetName(mVideoNumber + ".mp4");
        mPermissionHelper = new PermissionHelper(this);
 //录制之前删除所有的多余文件
        mFileUtils = new FileUtils(this);
        mFileUtils.deleteFile(mFileUtils.getMediaVideoPath(),null);
        mFileUtils.deleteFile(mFileUtils.getStorageDirectory(),null);

其中用来记录视频段的Mp4TsVideo类

/**
     * 记录下每段视频
     */
    private class Mp4TsVideo{
        /**
         * 视频段的地址
         */
        private String mp4Path;
        /**
         * ts地址
         */
        private String tsPath;
        /**
         * 是否需要翻转
         */
        private boolean flip;

        public String getMp4Path() {
            return mp4Path;
        }

        public void setMp4Path(String mp4Path) {
            this.mp4Path = mp4Path;
        }

        public String getTsPath() {
            return tsPath;
        }

        public void setTsPath(String tsPath) {
            this.tsPath = tsPath;
        }

        public boolean isFlip() {
            return flip;
        }

        public void setFlip(boolean flip) {
            this.flip = flip;
        }
    }
3.修改点击镜头切换的逻辑,在MainActivity中这个逻辑是直接停止录制,等待点击重新录制。本文这里,是切换摄像头成功后先保存当前录制的视频,然后再继续录制。
         case R.id.inversion:
                if(mMediaHelper.isRecording()){
                    mMediaHelper.stopRecordSave();
                    addMp4Video();
                    mVideoNumber++;
                    mMediaHelper.setTargetName(mVideoNumber+".mp4");
                    mMediaHelper.autoChangeCamera();
                    mMediaHelper.record();
                }else{
                    mMediaHelper.autoChangeCamera();
                }
                break;

其中addMp4Video()方法就是记录保存当前录制的视频段

/**
     * 记录这个视频片段并且开始处理。
     */
    private void addMp4Video(){
        Mp4TsVideo mp4TsVideo = new Mp4TsVideo();
        mp4TsVideo.setMp4Path(mMediaHelper.getTargetFilePath());
        mp4TsVideo.setTsPath(mFileUtils.getMediaVideoPath()+"/"+mVideoNumber+".ts");
        mp4TsVideo.setFlip(mMediaHelper.getPosition()== Camera.CameraInfo.CAMERA_FACING_FRONT);
        mTsVideo.add(mp4TsVideo);
        mp4ToTs();
    }

注意:之前就说过涉及到前置摄像头视频,所以需要翻转的功能来进行处理,翻转是需要重新编码,所以无法直接使用copy指令,所以转换ts的过程比较耗时,特别多段,为了保证体验的效率,所以拿到一段视频段就开始通过AsyncTask转换处理。

 /**
     * 如果发现是多个视频就异步开始合成,节省等待时间。
     * 通过递归的模式来处理视频合成。
     */
    private void mp4ToTs(){
        if(isCancel){
            return;
        }
        if(mTsVideo.size()==0){
            if(mTsPath.size()>0 && !mMediaHelper.isRecording()){
                showProgressLoading();
                concatVideo(mTsPath);
            }
            return;
        }
        final Mp4TsVideo mp4TsVideo = mTsVideo.get(0);
        Mp4TsVideo mp4TsVideoIng = (Mp4TsVideo) mStartVideo.getTag();
        if(mp4TsVideo == mp4TsVideoIng){
            return;
        }
        mStartVideo.setTag(mp4TsVideo);
        FFmpegRun.execute(FFmpegCommands.mp4ToTs(mp4TsVideo.getMp4Path(), mp4TsVideo.getTsPath(),mp4TsVideo.isFlip()), new FFmpegRun.FFmpegRunListener() {
            @Override
            public void onStart() {

            }

            @Override
            public void onEnd(int result) {
                if(mTsVideo.size() == 0 || isCancel){
                    return;
                }
                mTsPath.add(mp4TsVideo.getTsPath());
                mTsVideo.remove(mp4TsVideo);
                mp4ToTs();
            }

        });
    }

打开FFmpegCommands类新增mp4转ts的命令

 /**
     * mp4转ts
     * @param videoUrl
     * @param outPath
     * @param flip
     * @return
     */
    public static String[] mp4ToTs(String videoUrl,String outPath,boolean flip){
        Log.w("SLog","videoUrl:" + videoUrl + "\noutPath:" + outPath);
        ArrayList<String> _commands = new ArrayList<>();
        _commands.add("ffmpeg");
        _commands.add("-i");
        _commands.add(videoUrl);
        if(flip){
            _commands.add("-vf");
            _commands.add("hflip");
        }
        _commands.add("-b");
        _commands.add(String.valueOf(2 * 1024 * 1024));
        _commands.add("-s");
        _commands.add("720x1280");
        _commands.add("-acodec");
        _commands.add("copy");
//        _commands.add("-vcodec");
//        _commands.add("copy");
        _commands.add(outPath);
        String[] commands = new String[_commands.size()];
        for (int i = 0; i < _commands.size(); i++) {
            commands[i] = _commands.get(i);
        }
        return commands;
    }

注意:如果是前置录制的视频,需要镜像翻转,否则合成的视频有一段是倒过来,这样的视频完全不能到达要求 ,主要判断逻辑

       if(flip){
            _commands.add("-vf");
            //hflip左右翻转,vflip上下翻转
            _commands.add("hflip");
        }

完整的视频是按顺序拼接的,我通过递归的方式,一段一段的提取mTsVideo中的视频段,直到视频全部由mp4转成ts流为止。

4.录制视频段的行为和处理视频段的行为是互不干扰的,直到点击停止录制按钮,如果满足时间要求(我这里设置最低录制8秒),就只需要等待所有视频段转换完成。

点击停止按钮:

      case R.id.start_video_ing:
                if(mProgressNumber == 0){
                    stopView(false);
                    break;
                }
                Log.e("SLog","mProgressNumber:"+mProgressNumber);
                if (mProgressNumber < 8){
                    //时间太短不保存
                    Toast.makeText(this,"请至少录制到红线位置",Toast.LENGTH_LONG).show();
                    mMediaHelper.stopRecordUnSave();
                    stopView(false);
                    break;
                }
                //停止录制
                mMediaHelper.stopRecordSave();
                stopView(true);
                break;

stopView方法:

/**
     * 停止录制
     * @param isSave
     */
    private void stopView(boolean isSave){
        int timer = mProgressNumber;
        mProgressNumber = 0;
        mProgress.setProgress(0);
        handler.removeMessages(0);
        mTime.setText("00:00");
        mTime.setTag(timer);
        if(isSave) {
            String videoPath = mFileUtils.getMediaVideoPath();
            final File file = new File(videoPath);
            if(!file.exists()){
                Toast.makeText(this,"文件已损坏或者被删除,请重试!",Toast.LENGTH_SHORT).show();
                return;
            }
            File[] files = file.listFiles();
            if(files.length==1){
                startMediaVideo(mMediaHelper.getTargetFilePath());
            }else{
                showProgressLoading();
                addMp4Video();
            }
        }else{
            mFileUtils.deleteFile(mFileUtils.getStorageDirectory(),null);
            mFileUtils.deleteFile(mFileUtils.getMediaVideoPath(),null);
            mVideoNumber=1;
            isCancel = true;
        }
        mStartVideoIng.setVisibility(View.GONE);
        mStartVideo.setVisibility(View.VISIBLE);
    }

判断文件夹内如果只有一段视频,不需要做任何转换处理,直接进入下一步,这里和单段视频录制原理一样,如果是多段视频需要把最后一段视频也添加到待处理的集合中,等待递归处理完成。

处理完视频段后,得到所有视频段的ts文件,进入合成的方法concatVideo()

/**
     * ts合成视频
     * @param filePaths
     */
    private void concatVideo(List<String> filePaths){
        StringBuilder ts = new StringBuilder();
        for (String s:filePaths) {
            ts.append(s).append("|");
        }
        String tsVideo = ts.substring(0,ts.length()-1);
        final String videoPath = mFileUtils.getStorageDirectory()+"/video_ts.mp4";
        FFmpegRun.execute(FFmpegCommands.concatTsVideo(tsVideo, videoPath), new FFmpegRun.FFmpegRunListener() {
            @Override
            public void onStart() {
               Log.e("SLog","concatTsVideo start...");
            }

            @Override
            public void onEnd(int result) {
                Log.e("SLog","concatTsVideo end...");
                dismissProgress();
                startMediaVideo(videoPath);
            }
        });
    }

打开FFmpegCommands类新增ts合成mp4的命令

/**
     * ts拼接视频
     */
    public static String[] concatTsVideo(String _filePath, String _outPath) {//-f concat -i list.txt -c copy concat.mp4
        Log.w("SLog","_filePath:" + _filePath + "\n_outPath:" + _outPath);
        ArrayList<String> _commands = new ArrayList<>();
        _commands.add("ffmpeg");
        _commands.add("-i");
        _commands.add("concat:"+_filePath);
        _commands.add("-b");
        _commands.add(String.valueOf(2 * 1024 * 1024));
        _commands.add("-s");
        _commands.add("720x1280");
        _commands.add("-acodec");
        _commands.add("copy");
        _commands.add("-vcodec");
        _commands.add("copy");
        _commands.add(_outPath);
        String[] commands = new String[_commands.size()];
        for (int i = 0; i < _commands.size(); i++) {
            commands[i] = _commands.get(i);
        }
        return commands;
    }

因为之前mp4转ts的时候参数处理都一致,所以这里的ts流合成可以直接用copy指令直接复制音频和视频源,几乎秒合成。
合并完成后进入制作页面:

 /**
     * 进入下一步制作页面
     * @param path
     */
    private void startMediaVideo(String path){
        int timer = (int) mTime.getTag();
        Log.d("SLog","video path:"+path);
        Intent intent = new Intent(this,MakeVideoActivity.class);
        intent.putExtra("path",path);
        intent.putExtra("time",timer);
        startActivity(intent);
    }

制作页面的逻辑在前一篇文章已经实现,感兴趣的朋友自行查看

视频合成的功能是达到了,但是效率并不是最佳,特别在硬件差的手机上更是不敢恭维,我实现的途中尝试了很多办法,包括监听Camera的源数据处理,效果都不太好,所以如果哪位大神有更好的思路和方式,或者对ffmpeg命令的效率优化请简信我以及评论区讨论都可以。

最后我在提供一下其他我认为效率最佳的合成命令,也是官网查阅的。

/**
     * txt文件拼接视频
     */
    public static String[] concatPathVideo(String _filePath, String _outPath) {//-f concat -i list.txt -c copy concat.mp4
        if (SLog.debug) SLog.w("_filePath:" + _filePath + "\n_outPath:" + _outPath);
        ArrayList<String> _commands = new ArrayList<>();
        _commands.add("ffmpeg");
        _commands.add("-f");
        _commands.add("concat");
        _commands.add("-safe");
        _commands.add("0");
        _commands.add("-i");
        _commands.add(_filePath);
        _commands.add("-c");
        _commands.add("copy");
        _commands.add(_outPath);
        String[] commands = new String[_commands.size()];
        for (int i = 0; i < _commands.size(); i++) {
            commands[i] = _commands.get(i);
        }
        return commands;
    }

这里需要传入一个文件路径,这个文件的内容就是你合成视频的地址,多个视频换行区分,效率极高,但是有限制,比如帧率等参数一致才行(比如我都是用后置摄像头录制的视频段),否则合成的视频有问题或者无法播放。
simple.txt

file 'input1.mp4'
file 'input2.mp4'
file 'input3.mp4'

源码已经在之前的项目上更新,有朋友说找不到run方法或者so文件报错,这里的项目源码只提供了arm arm-v7的支持,如果你需要更多的框架支持,点这里Android Studio下编译FFmpeg so文件

我是源码

这应该是本年ffmpeg最后一篇,我对ffmpeg非常感兴趣,如果你也喜欢,欢迎讨论和指点,后面我的文章将会和穿戴设备Android Wear再续前缘

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

推荐阅读更多精彩内容