ndk吧 关注:3,608贴子:4,791
  • 2回复贴,共1

FFmpeg实现多段小视频合成

只看楼主收藏回复

这里主要实现一下多视频合成,主要困难是手机前置摄像头和后置摄像头录制的视频合成问题,我这里主要实现了功能,但是效率不优,暂时记录一下,如果有更好的方式再更新。
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 List<Mp4TsVideo> mTsVideo = new ArrayList<>();
/**
* mp4转ts流后的地址,主要合成的文件
*/
private List<String> mTsPath = 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;
}
}


IP属地:湖南1楼2018-05-19 11:50回复
    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()方法就是记录保存当前录制的视频段
    视音频交流群:2-0-⑤-5-0-④-5-6-〇
    /**
    * 记录这个视频片段并且开始处理。
    */
    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<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;
    }


    IP属地:湖南2楼2018-05-19 11:52
    回复
      注意:如果是前置录制的视频,需要镜像翻转,否则合成的视频有一段是倒过来,这样的视频完全不能到达要求 ,主要判断逻辑
      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){
      //时间太短不保存
      视音频交流群:②-0-⑤-5-0-④-5-6-〇
      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(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转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<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;
      }
      这里需要传入一个文件路径,这个文件的内容就是你合成视频的地址,多个视频换行区分,效率极高,但是有限制,比如帧率等参数一致才行(比如我都是用后置摄像头录制的视频段),否则合成的视频有问题或者无法播放。
      simple.txt
      file 'input1.mp4'
      file 'input2.mp4'
      file 'input3.mp4'


      IP属地:湖南3楼2018-05-19 11:53
      回复