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層已出,篇幅太長,下一篇,接著來(並附傳送門)!
加油!