Android通過FFmpeg實現(xiàn)多段小視頻合成

前面的文章可以說已經(jīng)入門绿聘,后面主要研究ffmpeg的深入功能以及對常用功能燥筷,這里主要實現(xiàn)一下多視頻合成,主要困難是手機前置攝像頭和后置攝像頭錄制的視頻合成問題志电,我這里主要實現(xiàn)了功能逢唤,但是效率不優(yōu)构罗,暫時記錄一下,如果有更好的方式再更新智玻。

如果本文的內(nèi)容沒有看明白,請先查看之前的文章
Android Studio下編譯FFmpeg so文件
Android通過FFmpeg實現(xiàn)小視頻音頻以及背景音樂合成

1.新建SelectRecordActivity類芙代,并且打開AndroidManifest.xml修改為啟動類(之前的啟動類是MainActivity吊奢,現(xiàn)在只是作為一個單獨的功能類),引用activity_select_record.xml布局文件纹烹,兩個選擇按鈕页滚。
image.png

點擊單段視頻錄制直接跳轉(zhuǎn)到MainActivity,其中的功能就是上面第二篇文章的內(nèi)容,本文主要實現(xiàn)第二按鈕的功能铺呵,點擊多段視頻錄制合成跳轉(zhuǎn)到MultiRecordActivity裹驰。

2.新建MultiRecordActivity,直接引用MainActivity的布局activity_main.xml片挂。因為布局都一樣幻林,主要區(qū)別在于修改切換攝像頭的按鈕邏輯以及停止錄制的邏輯贞盯。
image.png

需要用到變量

/**
     * 相機預(yù)覽
     */
    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;
    /**
     * 臨時記錄每段視頻的參數(shù)內(nèi)容
     */
    private List<Mp4TsVideo> mTsVideo = new ArrayList<>();
    /**
     * mp4轉(zhuǎn)ts流后的地址躏敢,主要合成的文件
     */
    private List<String> mTsPath = new ArrayList<>();
    /**
     * 是否已經(jīng)取消下一步,比如關(guān)閉了頁面整葡,就不再做線程處理件余,結(jié)束任務(wù)
     */
    private boolean isCancel;
    /**
     * 權(quán)限相關(guān)
     */
    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;
        /**
         * 是否需要翻轉(zhuǎn)
         */
        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中這個邏輯是直接停止錄制遭居,等待點擊重新錄制啼器。本文這里,是切換攝像頭成功后先保存當(dāng)前錄制的視頻俱萍,然后再繼續(xù)錄制端壳。
         case R.id.inversion:
                if(mMediaHelper.isRecording()){
                    mMediaHelper.stopRecordSave();
                    addMp4Video();
                    mVideoNumber++;
                    mMediaHelper.setTargetName(mVideoNumber+".mp4");
                    mMediaHelper.autoChangeCamera();
                    mMediaHelper.record();
                }else{
                    mMediaHelper.autoChangeCamera();
                }
                break;

其中addMp4Video()方法就是記錄保存當(dāng)前錄制的視頻段

/**
     * 記錄這個視頻片段并且開始處理。
     */
    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();
    }

注意:之前就說過涉及到前置攝像頭視頻鼠次,所以需要翻轉(zhuǎn)的功能來進行處理更哄,翻轉(zhuǎn)是需要重新編碼,所以無法直接使用copy指令腥寇,所以轉(zhuǎn)換ts的過程比較耗時成翩,特別多段,為了保證體驗的效率赦役,所以拿到一段視頻段就開始通過AsyncTask轉(zhuǎn)換處理麻敌。

 /**
     * 如果發(fā)現(xiàn)是多個視頻就異步開始合成,節(jié)省等待時間掂摔。
     * 通過遞歸的模式來處理視頻合成术羔。
     */
    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轉(zhuǎn)ts的命令

 /**
     * mp4轉(zhuǎn)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;
    }

注意:如果是前置錄制的視頻,需要鏡像翻轉(zhuǎn)乙漓,否則合成的視頻有一段是倒過來级历,這樣的視頻完全不能到達要求 ,主要判斷邏輯

       if(flip){
            _commands.add("-vf");
            //hflip左右翻轉(zhuǎn)叭披,vflip上下翻轉(zhuǎn)
            _commands.add("hflip");
        }

完整的視頻是按順序拼接的寥殖,我通過遞歸的方式,一段一段的提取mTsVideo中的視頻段涩蜘,直到視頻全部由mp4轉(zhuǎn)成ts流為止嚼贡。

4.錄制視頻段的行為和處理視頻段的行為是互不干擾的,直到點擊停止錄制按鈕同诫,如果滿足時間要求(我這里設(shè)置最低錄制8秒)粤策,就只需要等待所有視頻段轉(zhuǎn)換完成。

點擊停止按鈕:

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

判斷文件夾內(nèi)如果只有一段視頻秩贰,不需要做任何轉(zhuǎn)換處理,直接進入下一步熊户,這里和單段視頻錄制原理一樣萍膛,如果是多段視頻需要把最后一段視頻也添加到待處理的集合中,等待遞歸處理完成嚷堡。

處理完視頻段后蝗罗,得到所有視頻段的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轉(zhuǎn)ts的時候參數(shù)處理都一致蝌戒,所以這里的ts流合成可以直接用copy指令直接復(fù)制音頻和視頻源串塑,幾乎秒合成。
合并完成后進入制作頁面:

 /**
     * 進入下一步制作頁面
     * @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);
    }

制作頁面的邏輯在前一篇文章已經(jīng)實現(xiàn)北苟,感興趣的朋友自行查看

視頻合成的功能是達到了桩匪,但是效率并不是最佳,特別在硬件差的手機上更是不敢恭維友鼻,我實現(xiàn)的途中嘗試了很多辦法傻昙,包括監(jiān)聽Camera的源數(shù)據(jù)處理,效果都不太好彩扔,所以如果哪位大神有更好的思路和方式妆档,或者對ffmpeg命令的效率優(yōu)化請簡信我以及評論區(qū)討論都可以。

最后我在提供一下其他我認為效率最佳的合成命令虫碉,也是官網(wǎng)查閱的贾惦。

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

這里需要傳入一個文件路徑,這個文件的內(nèi)容就是你合成視頻的地址敦捧,多個視頻換行區(qū)分须板,效率極高,但是有限制兢卵,比如幀率等參數(shù)一致才行(比如我都是用后置攝像頭錄制的視頻段)习瑰,否則合成的視頻有問題或者無法播放。
simple.txt

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

源碼已經(jīng)在之前的項目上更新秽荤,有朋友說找不到run方法或者so文件報錯甜奄,這里的項目源碼只提供了arm arm-v7的支持,如果你需要更多的框架支持王滤,點這里Android Studio下編譯FFmpeg so文件

我是源碼

這應(yīng)該是本年ffmpeg最后一篇滓鸠,我對ffmpeg非常感興趣雁乡,如果你也喜歡,歡迎討論和指點糜俗,后面我的文章將會和穿戴設(shè)備Android Wear再續(xù)前緣踱稍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末曲饱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子珠月,更是在濱河造成了極大的恐慌扩淀,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啤挎,死亡現(xiàn)場離奇詭異驻谆,居然都是意外死亡,警方通過查閱死者的電腦和手機庆聘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門胜臊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伙判,你說我怎么就攤上這事象对。” “怎么了宴抚?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵勒魔,是天一觀的道長。 經(jīng)常有香客問我菇曲,道長冠绢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任羊娃,我火速辦了婚禮唐全,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蕊玷。我一直安慰自己邮利,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布垃帅。 她就那樣靜靜地躺著延届,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贸诚。 梳的紋絲不亂的頭發(fā)上方庭,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音酱固,去河邊找鬼械念。 笑死,一個胖子當(dāng)著我的面吹牛运悲,可吹牛的內(nèi)容都是我干的龄减。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼班眯,長吁一口氣:“原來是場噩夢啊……” “哼希停!你這毒婦竟也來了烁巫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤宠能,失蹤者是張志新(化名)和其女友劉穎亚隙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體违崇,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡阿弃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亦歉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恤浪。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肴楷,靈堂內(nèi)的尸體忽然破棺而出水由,到底是詐尸還是另有隱情,我是刑警寧澤赛蔫,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布砂客,位于F島的核電站,受9級特大地震影響呵恢,放射性物質(zhì)發(fā)生泄漏鞠值。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一渗钉、第九天 我趴在偏房一處隱蔽的房頂上張望彤恶。 院中可真熱鬧,春花似錦鳄橘、人聲如沸声离。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽术徊。三九已至,卻和暖如春鲸湃,著一層夾襖步出監(jiān)牢的瞬間赠涮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工暗挑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留笋除,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓炸裆,卻偏偏與公主長得像垃它,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容