这里主要实现一下多视频合成,主要困难是手机前置摄像头和后置摄像头录制的视频合成问题,我这里主要实现了功能,但是效率不优,暂时记录一下,如果有更好的方式再更新。
1.新建SelectRecordActivity类,并且打开AndroidManifest.xml修改为启动类(之前的启动类是MainActivity,现在只是作为一个单独的功能类),引用activity_select_record.xml布局文件,两个选择按钮。
点击单段视频录制实现第二按钮的功能,点击多段视频录制合成跳转到MultiRecordActivity。
2.新建MultiRecordActivity,直接引用MainActivity的布局activity_main.xml。因为布局都一样,主要区别在于修改切换摄像头的按钮逻辑以及停止录制的逻辑。
需要用到变量
/** * 相机预览 */ 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 ListmTsVideo = new ArrayList<>(); /** * mp4转ts流后的地址,主要合成的文件 */ private ListmTsPath = 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_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(ListfilePaths){ 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_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的源数据处理,效果都不太好,所以如果哪位大神有更好的思路和方式。
最后我在提供一下其他我认为效率最佳的合成命令,也是官网查阅的。
/** * 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_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'
原创作者:Galaxy北爱 原文链接:https://www.jianshu.com/p/6c51b11550be 校验:逆流的鱼yuiop