java使用ffmpeg合成拼接MP4视频
目录
需求背景
项目中需要将移动端(IOS/安卓)的MP4视频多次合成为一个视频,视频不能直接合成,比如安卓
的两个视频合成后变成横屏。
ios
也是如此,因为ios
和安卓拍出来的视频有旋转角度Rotation
,安卓竖屏旋转 90.0 ios竖屏旋转 -90.0,还有安卓
和ios
两种视频合并情况。
ios和安卓需要设置视频分辨率和帧率尽量保持一致,否则视频合成会出现很多未知问题
实现步骤
- 检查视频旋转属性
- 视频去掉旋转属性
- 旋转视频
- 创建合成视频txt文件
- 合成视频
视频处理类库
<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
协议,实际上就只是把两个视频直接拼接,把后一个视频直接贴到前一个视频后面而已,因此只会适用于ts
和flv
等一些格式。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 视频旋转处理命令
这里博主讲了手机录制视频旋转角问题
视频预处理
touch fish