1. 程式人生 > >android視訊的編輯(錄製,裁剪,合成)(1)

android視訊的編輯(錄製,裁剪,合成)(1)

好久沒寫部落格了,最近的事情的比較多。公司正在向產品這塊轉型,要做音視訊的編輯開發,之前的接觸這塊的東西並不多,所以開發起來有很多的困難,從踩自定義相機的坑開始,視訊的錄製,編輯(主要包括合成和裁剪);音訊的錄製,裁剪;圖片的一些基本處理,包括裁剪,旋轉,新增文字,水印等等。哇,真的很麻煩!更令人鬧心的是,之前和我合作的,主要開發視訊這塊的功能的同事,頂不住壓力,拉稀了,不幹了!那。。。視訊這塊的開發只能又落到我的頭上了!

這裡寫圖片描述

廢話就扯到這裡,進入正題。

視訊的合成裁剪,一般都用的FFmpeg,這塊的c程式碼我沒研究過,但是呢,為了開發進度能夠順利的進行,我是不可能自己進行編譯一遍FFmpeg的,於是就從萬能的gitHub上找了一份編譯好的來用(其實我心裡的是鄙視我自己的!)。下面開始擼程式碼!

1.視訊的採集功能!

怎麼說呢,視訊的採集看似簡單,其實比較麻煩!從自定義相機開始出發,當然,我沒自己從頭開始寫,用的是之前那個孩子的程式碼!,改了幾天,發現各種各樣的問題,採集視訊的播放方向,不同相機的不同攝像頭的解析度設定,以及採集完成後,編輯後的播放方向問題等等。加上定時採集,回刪操作,拍攝過程中的標記新增等等!總之呢,大坑到沒有,小坑不斷!之後的合成更是慢的一批,產品經理用了一下,否了!那。。很尷尬!視訊的功能做不出來,那就沒用!但是國內功能稍微好點的視訊編輯都收費,領導在github上看到某雲的sdk編輯是免費的,但是他們的雲是收費的,感覺能搞,於是讓我先。。。。。加上!(欲哭無淚)先加上。。。。結果週五過來一談,嗯,必須用他們的雲,其他的免費!我忍著不笑!可憐我剛剛加上採集。採集確實好用,並且不收費!
下面程式碼:

佈局檔案程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" 
    android:id="@+id/rl_root"

    >

    <android.opengl.GLSurfaceView
android:id="@+id/camera_preview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentBottom="true" android:layout_alignParentTop="true"/>
<com.tian.videomergedemo.view.CameraHintView android:id="@+id/camera_hint" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentBottom="true" android:layout_alignParentTop="true" /> <RelativeLayout android:id="@+id/bottom_mask" android:layout_width="fill_parent" android:layout_height="120.0dip" android:layout_alignParentBottom="true" android:layout_gravity="bottom" > <ImageView android:id="@+id/iv_record" android:layout_width="75dp" android:layout_height="75dp" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:src="@drawable/record_state" /> <ImageView android:id="@+id/iv_point_maker1" android:layout_width="40dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginRight="30dp" android:layout_toLeftOf="@id/iv_record" android:src="@drawable/record_maker_d" /> <ImageView android:id="@+id/iv_stop" android:layout_width="40dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginLeft="30dp" android:layout_toRightOf="@id/iv_record" android:src="@drawable/record_stop_ok" /> </RelativeLayout> <RelativeLayout android:id="@+id/rl_progress_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" > <com.tian.videomergedemo.view.RecordProgressView android:id="@+id/record_progress" android:layout_width="match_parent" android:layout_height="13dp" /> </RelativeLayout> <RelativeLayout android:id="@+id/rl" android:layout_width="match_parent" android:layout_height="50dp" android:layout_below="@id/rl_progress_bar" > <Chronometer android:id="@+id/tv_record_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:drawableLeft="@drawable/red_dots" android:drawablePadding="5dp" android:format="%s" android:gravity="center" android:textColor="@color/red" android:textSize="19sp"/> <TextView android:id="@+id/tv_record_time1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:drawableLeft="@drawable/red_dots" android:drawablePadding="5dp" android:text="00:00" android:visibility="gone" android:textColor="@android:color/white" android:textSize="19sp" /> </RelativeLayout> <LinearLayout android:id="@+id/ll_top" android:layout_width="match_parent" android:layout_height="50dp" android:background="#607394" android:gravity="center_vertical" android:orientation="horizontal" > <ImageView android:id="@+id/iv_back" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="15dp" android:src="@drawable/record_cha" /> <ImageView android:id="@+id/iv_flash" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="15dp" android:src="@drawable/record_falsh_state" /> <ImageView android:id="@+id/iv_camera_switch" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="15dp" android:src="@drawable/icn_change_view" /> <ImageView android:id="@+id/iv_clock" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="15dp" android:src="@drawable/clock" /> <TextView android:id="@+id/tv_resolution" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="480P" android:textColor="@android:color/white" android:textSize="16sp" /> </LinearLayout> <RelativeLayout android:id="@+id/rl_progress" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" > <ProgressBar android:id="@+id/progressBar1" style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> </RelativeLayout> </RelativeLayout>

效果圖:

這裡寫圖片描述

開始錄製引數設定(也可以設定其他的引數,其他的需求請自行查閱API):

    /**
     * 初始化預設錄製引數
     */
    private void initData() {
        mShortVideoConfig = new ShortVideoConfig();
        //幀率   
        mShortVideoConfig.fps = 20;
        //視訊的位元速率
        mShortVideoConfig.videoBitrate = 1000;
        //音訊的位元速率
        mShortVideoConfig.audioBitrate = 64;
        //預設的視訊錄製的解析度(480)
        mShortVideoConfig.resolution = StreamerConstants.VIDEO_RESOLUTION_480P;
        //H264編碼
        mShortVideoConfig.encodeType = AVConst.CODEC_ID_AVC;
        //功率(預設平衡模式)
        mShortVideoConfig.encodeProfile = VideoEncodeFormat.ENCODE_PROFILE_BALANCE;
        //預設軟編模式
        mShortVideoConfig.encodeMethod = StreamerConstants.ENCODE_METHOD_SOFTWARE;

    }

初始化相機操作:

/**
     * 初始化相機,開始拍攝工作
     */
    private void initCameraData() {
        mKSYRecordKit.setPreviewFps(mShortVideoConfig.fps);
        mKSYRecordKit.setTargetFps(mShortVideoConfig.fps);
        mKSYRecordKit.setVideoKBitrate(mShortVideoConfig.videoBitrate);
        mKSYRecordKit.setAudioKBitrate(mShortVideoConfig.audioBitrate);
        mKSYRecordKit.setPreviewResolution(mShortVideoConfig.resolution);
        mKSYRecordKit.setTargetResolution(mShortVideoConfig.resolution);
        mKSYRecordKit.setVideoCodecId(mShortVideoConfig.encodeType);
        mKSYRecordKit.setEncodeMethod(mShortVideoConfig.encodeMethod);
        mKSYRecordKit.setVideoEncodeProfile(mShortVideoConfig.encodeProfile);
        mKSYRecordKit.setRotateDegrees(0);
        mKSYRecordKit.setDisplayPreview(mCameraPreviewView);
        mKSYRecordKit.setEnableRepeatLastFrame(false);
        mKSYRecordKit.setCameraFacing(CameraCapture.FACING_FRONT);
        mKSYRecordKit.setFrontCameraMirror(false);
        mKSYRecordKit.setOnInfoListener(mOnInfoListener);
        mKSYRecordKit.setOnErrorListener(mOnErrorListener);
        mKSYRecordKit.setOnLogEventListener(mOnLogEventListener);


        CameraTouchHelper cameraTouchHelper = new CameraTouchHelper();
        cameraTouchHelper.setCameraCapture(mKSYRecordKit.getCameraCapture());
        mCameraPreviewView.setOnTouchListener(cameraTouchHelper);
        cameraTouchHelper.setCameraHintView(mCameraHintView);

        mKSYRecordKit.startCameraPreview();

    }

開始拍攝:

private void startRecord() {

        mRecordUrl = getRecordFileFolder() + "/" + System.currentTimeMillis() + ".mp4";

        videosToMerge.add(mRecordUrl);//每次開始錄製時記錄

        mKSYRecordKit.setVoiceVolume(50);
        mKSYRecordKit.startRecord(mRecordUrl);
        mIsFileRecording = true;
        mRecordControler.getDrawable().setLevel(2);
    }

暫停拍攝(因為是斷點拍攝,finished引數判斷是不是最後的錄製完成標記):

/**
     * 
     * @param finished
     */
    private void stopRecord(boolean finished) {
        //錄製完成進入編輯
        //若錄製檔案大於1則需要觸發檔案合成
        if (finished) {
            if (mKSYRecordKit.getRecordedFilesCount() > 1) {
                String fileFolder = getRecordFileFolder();
                //合成檔案路徑
                final String outFile = fileFolder + "/" + "merger_" + System.currentTimeMillis() + ".mp4";

                mKSYRecordKit.stopRecord(outFile, new KSYRecordKit.MegerFilesFinishedListener() {
                    @Override
                    public void onFinished() {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                //TODO
                                Toast.makeText(RecordActivity.this, "短視訊錄製結束!", Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
                });
            } else {
                mKSYRecordKit.stopRecord();
            }

        } else {
            //普通錄製停止
            mKSYRecordKit.stopRecord();
        }
        //更新進度顯示
        mRecordProgressCtl.stopRecording();
        mRecordControler.getDrawable().setLevel(1);
        updateDeleteView();
        mIsFileRecording = false;
        stopChronometer();
    }

合成的非同步任務棧

private class MergeVideos extends AsyncTask<String, Integer, String> {

        //The working path where the video files are located
        private String workingPath; 
        //The file names to merge
        private ArrayList<String> videosToMerge;
        //Dialog to show to the user
        private ProgressDialog progressDialog;

        private MergeVideos(String workingPath, ArrayList<String> videosToMerge) {
            this.workingPath = workingPath;
            this.videosToMerge = videosToMerge;
        }

        @Override
        protected void onPreExecute() {
            if(progressDialog==null){
                progressDialog = ProgressDialog.show(RecordActivity.this,
                        "合併中...", "請稍等...", true);
            }else{
                progressDialog.show();
            }

        };

        @Override
        protected String doInBackground(String... params) {
            File storagePath = new File(workingPath);             
            storagePath.mkdirs();  
            File myMovie = new File(storagePath, String.format("output-%s.mp4", newName)); 
            finalPath=myMovie.getAbsolutePath();
            VideoStitchingRequest videoStitchingRequest = new VideoStitchingRequest.Builder()
            .inputVideoFilePath(videosToMerge)
            .outputPath(finalPath).build();
            FfmpegManager manager = FfmpegManager.getInstance();
            manager.stitchVideos(RecordActivity.this, videoStitchingRequest,
            new CompletionListener() {
                @Override
                public void onProcessCompleted(String message,List<String> merger) {
                    mMessage=message;
                }

            });
            return mMessage;
        }

        @Override
        protected void onPostExecute(String value) {
            super.onPostExecute(value);
            progressDialog.dismiss();
            progressDialog.cancel();
            progressDialog=null;
            if(value!=null){
            Toast.makeText(RecordActivity.this, "啊哦,錄製失敗了!請重新嘗試...", Toast.LENGTH_SHORT).show();
            }else{
                saveFlagPointer(mRecordProgressCtl.getFlagPointers());
                Intent intent = new Intent(RecordActivity.this,EditVedioActivity.class);
                intent.putExtra("vedio_path",finalPath);//把最終的路徑傳過去
                startActivity(intent);
                finish();
            }
        }

    }

錄製完成後會跳轉到編輯介面,合成的操作本來就在子執行緒中,這裡其實是多餘啟動的一個子執行緒合成,程式碼後期還會不斷的優化的,勿噴!

package com.tian.videomergedemo.manager;

import java.io.File;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.annotation.SuppressLint;
import android.content.Context;

import com.tian.videomergedemo.R;
import com.tian.videomergedemo.inter.CompletionListener;
import com.tian.videomergedemo.task.StitchingTask;
import com.tian.videomergedemo.task.TrimTask;
import com.tian.videomergedemo.utils.Utils;


/**
 * Created by TCX on 21/01/17.
 * 
 * 合併和切割的操做(如果編碼的話會很耗時,所以用執行緒池進行管理控制)
 * 
 * 
 */
public class FfmpegManager {

    private static FfmpegManager manager;

    private String mFfmpegInstallPath;

    private static int NUMBER_OF_CORES =
            Runtime.getRuntime().availableProcessors();
    // 執行緒佇列
    private final BlockingQueue<Runnable> mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>();
    private static final int KEEP_ALIVE_TIME = 1;
    // 執行緒池設定
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // 執行緒池管理
    ThreadPoolExecutor mDecodeThreadPool = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // 初始化執行緒
            NUMBER_OF_CORES,       // 最大執行緒數
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            mDecodeWorkQueue);

    private FfmpegManager() {

    }
    public synchronized static FfmpegManager getInstance() {

        if (manager == null) {
            manager = new FfmpegManager();

        }
        return manager;
    }




    /**
     * 合併操作
     * @param context
     * @param videoStitchingRequest
     * @param completionListener
     */
    public void stitchVideos(Context context, VideoStitchingRequest videoStitchingRequest, CompletionListener completionListener) {
        installFfmpeg(context);
        StitchingTask stitchingTask = new StitchingTask(context, mFfmpegInstallPath, videoStitchingRequest, completionListener);
        mDecodeThreadPool.execute(stitchingTask);
    }

    //切割操作
    public void trimVideo(Context context,File srcFile,File destFile,List<long[]> mNewSeeks,CompletionListener completionListener){
         installFfmpeg(context);
         TrimTask trimTask=new TrimTask(context, mFfmpegInstallPath, srcFile, destFile,mNewSeeks , completionListener);
         mDecodeThreadPool.execute(trimTask);
    }

    /*
    * 插入FFmpeg的路徑(這裡我儲存在資原始檔下的raw資料夾下)
    */
    @SuppressLint("NewApi") private void installFfmpeg(Context context) {

        String arch = System.getProperty("os.arch");//獲取CPU的架構型別
        String arc = arch.substring(0, 3).toUpperCase();
        String rarc = "";
        int rawFileId;
        if (arc.equals("ARM")) {//arm架構
            rawFileId = R.raw.ffmpeg;
        } else if (arc.equals("MIP")) {
            rawFileId = R.raw.ffmpeg;
        } else if (arc.equals("X86")) {//x86架構
            rawFileId = R.raw.ffmpeg_x86;
        } else {
            rawFileId = R.raw.ffmpeg;
        }

        File ffmpegFile = new File(context.getCacheDir(), "ffmpeg");
        mFfmpegInstallPath = ffmpegFile.toString();
        Utils.installBinaryFromRaw(context, rawFileId, ffmpegFile);
        ffmpegFile.setExecutable(true);//對操作者的執行許可權
    }

}

合成的執行緒

package com.tian.videomergedemo.task;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.tian.videomergedemo.inter.CompletionListener;
import com.tian.videomergedemo.manager.VideoStitchingRequest;

/**
 * Created by TCX on 22/01/16.
 */
public class StitchingTask implements Runnable {

    private Context context;
    private VideoStitchingRequest videoStitchingRequest;
    private CompletionListener completionListener;
    private String mFfmpegInstallPath;

    public StitchingTask(Context context, String mFfmpegInstallPath, VideoStitchingRequest stitchingRequest, CompletionListener completionListener) {
        this.context = context;
        this.mFfmpegInstallPath = mFfmpegInstallPath;
        this.videoStitchingRequest = stitchingRequest;
        this.completionListener = completionListener;
    }


    @Override
    public void run() {
        stitchVideo(context, mFfmpegInstallPath, videoStitchingRequest, completionListener);
    }


    private void stitchVideo(Context context, String mFfmpegInstallPath, VideoStitchingRequest videoStitchingRequest, final CompletionListener completionListener) {


        //合成的路徑
        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "ffmpeg_videos";
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File inputfile = new File(path, "input.txt");

        try {
            inputfile.createNewFile();
            FileOutputStream out = new FileOutputStream(inputfile);
            for (String string : videoStitchingRequest.getInputVideoFilePaths()) {
                out.write(("file " + "'" + string + "'").getBytes());
                out.write("\n".getBytes());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
//        execFFmpegBinary("-i " + src.getAbsolutePath() + " -ss "+ startMs/1000 + " -to " + endMs/1000 + " -strict -2 -async 1 "+ dest.getAbsolutePath());


        //合成的FFmpeg命令列
        String[] sampleFFmpegcommand = {mFfmpegInstallPath, "-f", "concat", "-i", inputfile.getAbsolutePath(), "-c", "copy", videoStitchingRequest.getOutputPath()};
        try {
            Process ffmpegProcess = new ProcessBuilder(sampleFFmpegcommand)
                    .redirectErrorStream(true).start();

            String line;

            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(ffmpegProcess.getInputStream()));
            Log.d("***", "*******Starting FFMPEG");
            while ((line = reader.readLine()) != null) {

                Log.d("***", "***" + line + "***");
            }
            Log.d(null, "****ending FFMPEG****");

            ffmpegProcess.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }

        inputfile.delete();
        //合成成功的介面回撥
        completionListener.onProcessCompleted("Video Stitiching Comleted",null);

    }
}

都有註釋,不用我多扯皮了吧!

OK,至此,合成和裁剪的java層已出,篇幅太長,下一篇,接著來(並附傳送門)!

加油!