java使用ffmpeg合成拼接MP4视频

目录

需求背景

项目中需要将移动端(IOS/安卓)的MP4视频多次合成为一个视频,视频不能直接合成,比如安卓的两个视频合成后变成横屏。
ios也是如此,因为ios和安卓拍出来的视频有旋转角度Rotation,安卓竖屏旋转 90.0 ios竖屏旋转 -90.0,还有安卓ios两种视频合并情况。
ios和安卓需要设置视频分辨率和帧率尽量保持一致,否则视频合成会出现很多未知问题

实现步骤

  1. 检查视频旋转属性
  2. 视频去掉旋转属性
  3. 旋转视频
  4. 创建合成视频txt文件
  5. 合成视频

视频处理类库

   <dependency>
       <!--java封装各种视频处理库,包括ffmpeg-->
      <groupId>org.bytedeco</groupId>
          <artifactId>javacv-platform</artifactId>
          <version>1.5.9</version>
      </dependency>
      <!--java封装ffmpeg命令-->
      <dependency>
          <groupId>ws.schild</groupId>
          <artifactId>jave-core</artifactId>
          <version>3.3.1</version>
      </dependency>
      <dependency>
          <groupId>ws.schild</groupId>
          <artifactId>jave-nativebin-linux64</artifactId>
          <version>3.3.1</version>
      </dependency>

代码实现

1.检查视频旋转属性

使用视频播放器(potplayer)也可以看到

     private static final String SUFFIX = ".mp4";

     /**
     * 获取视频旋转属性 Rotation 安卓竖屏旋转 90.0 ios竖屏旋转 -90.0
     *
     * @return 旋转角度
     */
    public static int getAudioRotation(String path) throws FFmpegFrameGrabber.Exception {
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(path);
        grabber.start();
        int displayRotation = (int) grabber.getDisplayRotation();
        grabber.stop();
        grabber.release();
        return displayRotation;
    }

2.视频去掉旋转属性

视频去掉旋转属性后会变成横屏,所以需要下一步的将横屏旋转为竖屏

     private static String removeRotation(String path, String tmpPath, Integer count) throws Exception {
        ProcessWrapper executor = new DefaultFFMPEGLocator().createExecutor();
        String removeRotatePath = Path.of(tmpPath + "removeRotate" + count + SUFFIX).toString();
        executor.addArgument("-i");
        executor.addArgument(path);
        executor.addArgument("-metadata:s:v");
        executor.addArgument("rotate=0");
        executor.addArgument("-c");
        executor.addArgument("copy");
        executor.addArgument(removeRotatePath);
        executor.execute();

        try (BufferedReader br = new BufferedReader(new InputStreamReader(executor.getErrorStream()))) {
            blockFfmpeg(br);
        }

        executor.close();
        return removeRotatePath;
    }


    /**
     * 等待命令执行成功,退出
     *
     * @throws IOException
     */
    private static void blockFfmpeg(BufferedReader br) throws IOException {
        String line;
        // 该方法阻塞线程,直至合成成功
        while ((line = br.readLine()) != null) {
            doNothing(line);
        }
    }

3. 旋转视频

将横屏旋转为竖屏,这一步会对视频重新编码,所以处理会慢一些

  /**
     * 旋转mp4视频
     * transpose=1 左旋
     * transpose=2 右旋
     */
    private static String transposeMp4(String path, String tmpPath, Integer count) throws Exception {
        int audioRotation = getAudioRotation(path);
        if (audioRotation == 0) {
            return path;
        }

        String removeRotatePath = removeRotation(path, tmpPath, count);
        String transposePath = Path.of(tmpPath + "transpose" + count + SUFFIX).toString();

        int transpose = 2;
        if (audioRotation == 90) {
            transpose = 2;
        }
        if (audioRotation == -90) {
            transpose = 1;
        }

        ProcessWrapper executor = new DefaultFFMPEGLocator().createExecutor();
        executor.addArgument("-i");
        executor.addArgument(removeRotatePath);
        executor.addArgument("-vf");
        executor.addArgument("transpose=" + transpose);
        executor.addArgument(transposePath);
        executor.execute();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(executor.getErrorStream()))) {
            blockFfmpeg(br);
        }

        executor.close();

        Files.deleteIfExists(Path.of(removeRotatePath));
        return transposePath;
    }

视频预处理

如果视频中出现卡帧或者其他问题,可以使用此方法进行预处理

    /**
     * 合并不是同一设备录制的、格式不同的视频文件 待拼接的文件预先处理一下
     * 比如分辨率不同
     */
    private static String preHandleVideo(String path, String tmpPath, Integer count) throws IOException {
        ProcessWrapper executor = new DefaultFFMPEGLocator().createExecutor();
        String preHandlePath = Path.of(tmpPath + "preHandle" + count + SUFFIX).toString();
        executor.addArgument("-i");
        executor.addArgument(path);
        executor.addArgument("-c");
        executor.addArgument("copy");
        executor.addArgument("-bsf:v");
        executor.addArgument("h264_mp4toannexb");
        executor.addArgument("-f");
        executor.addArgument("mpegts");
        executor.addArgument(preHandlePath);
        executor.execute();

        try (BufferedReader br = new BufferedReader(new InputStreamReader(executor.getErrorStream()))) {
            blockFfmpeg(br);
        }
        executor.close();

        return preHandlePath;
    }

4. 创建合成视频txt文件

如果直接使用下面命令 ,出现的报错,Found duplicated MOOV Atom. Skipped,结果只是把第一个视频拷贝一遍就结束了。 原理上是因为concat协议,实际上就只是把两个视频直接拼接,把后一个视频直接贴到前一个视频后面而已,因此只会适用于tsflv等一些格式。mp4格式整体有一层容器,而不像ts这类格式可以直接拼接,需要先解开容器再对提取的视频流进行拼接。

ffmpeg -i concat a.mp4|b.mp4 -c copy out.mp4

换一种处理方式,对容器进行处理,具体操作方式如下。创建一个mylist.txt文件如下:

file '/path/to/file1'
file '/path/to/file2'
file '/path/to/file3'

执行下面的命令

ffmpeg -f concat -safe 0 -i mylist.txt -c copy output

java代码创建文件

  private static String createConcatTxtFile(List<String> files, String tmpPath) {
        String txtFile = tmpPath + ".txt";
        StringBuilder content = new StringBuilder();
        for (String file : files) {
            content.append("file '").append(file).append("'\r\n");
        }

        FileUtil.writeUtf8String(content.toString(), txtFile);
        return txtFile;
    }

5. 合成视频

    private static String concatVideo(String txtPath, String tmpPath) throws IOException {
        String outputFile = Path.of(tmpPath + SUFFIX).toString();
        ProcessWrapper executor = new DefaultFFMPEGLocator().createExecutor();
        executor.addArgument("-f");
        executor.addArgument("concat");
        executor.addArgument("-safe");
        executor.addArgument("0");
        executor.addArgument("-i");
        executor.addArgument(txtPath);
        executor.addArgument("-c");
        executor.addArgument("copy");
        executor.addArgument(outputFile);
        executor.execute();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(executor.getErrorStream()))) {
            blockFfmpeg(br);
        }

        executor.close();
        return outputFile;
    }

6. 方法组合

  public static String compositeVideo(String preFile, String currentFile) {
        String mergeFilePath = "";
        try {
            String outputPath = Path.of(FileUtil.getTmpDirPath()).toString();
            String uuid = UUID.randomUUID().toString();

            String localMp41 = Path.of(outputPath, UUID.randomUUID() + SUFFIX).toString();
            String localMp42 = Path.of(outputPath, UUID.randomUUID() + SUFFIX).toString();

            MinioFileUtils.downloadLocal(preFile, localMp41);
            MinioFileUtils.downloadLocal(currentFile, localMp42);

            String localMp41Trans = transposeMp4(localMp41, Path.of(outputPath, uuid).toString(), 1);
            String localMp42Trans = transposeMp4(localMp42, Path.of(outputPath, uuid).toString(), 2);

            List<String> list = List.of(localMp41Trans, localMp42Trans);
            String concatTxtFile = createConcatTxtFile(list, Path.of(outputPath, uuid).toString());
            String concatPath = concatVideo(concatTxtFile, Path.of(outputPath, uuid).toString());

            mergeFilePath = MinioFileUtils.upload(concatPath);
            Files.deleteIfExists(Path.of(localMp41));
            Files.deleteIfExists(Path.of(localMp41Trans));
            Files.deleteIfExists(Path.of(localMp42));
            Files.deleteIfExists(Path.of(localMp42Trans));
            Files.deleteIfExists(Path.of(concatPath));
            Files.deleteIfExists(Path.of(concatTxtFile));

        } catch (Exception e) {
            log.error("视频合成失败", e);
        }

        return mergeFilePath;
    }

参考链接

FFmpeg文档
FFmpeg 视频旋转处理命令
这里博主讲了手机录制视频旋转角问题
视频预处理

posted @ 2023-08-26 23:22  meow_world  阅读(811)  评论(0)    收藏  举报