前面的文章可以說已經(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布局文件纹烹,兩個選擇按鈕页滚。
點擊單段視頻錄制直接跳轉(zhuǎn)到MainActivity,其中的功能就是上面第二篇文章的內(nèi)容,本文主要實現(xiàn)第二按鈕的功能铺呵,點擊多段視頻錄制合成跳轉(zhuǎn)到MultiRecordActivity裹驰。
2.新建MultiRecordActivity,直接引用MainActivity的布局activity_main.xml片挂。因為布局都一樣幻林,主要區(qū)別在于修改切換攝像頭的按鈕邏輯以及停止錄制的邏輯贞盯。
需要用到變量
/**
* 相機預(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ù)前緣踱稍。