spring boot 集成 ffmpeg
什么是 ffmpeg
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
ffmpeg 使用C++ 开发,所有功能集成在一个exe程序里面,对我们调用者来说就是一串命令,所以集成方式有很多种,最底层就是使用java去执行拼接好的命令。
下载ffmpeg
下载地址 https://github.com/BtbN/FFmpeg-Builds/releases
下载对应系统版本。
解压出来后有三个exe文件,ffmpeg(音视频编辑工具),ffplay(音视频解码播放工具),ffprobe(流媒体分析工具),这里我们只使用ffmpeg
将 ffmpeg 加入系统环境变量,方便全局调用。
方法一
添加额外命令行拼接jar
- 添加maven
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.0.1</version>
<exclusions>
<!-- 排除windows 32位系统 -->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win32</artifactId>
</exclusion>
<!-- 排除linux 32位系统 -->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux32</artifactId>
</exclusion>
<!-- 排除Mac系统-->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osx64</artifactId>
</exclusion>
</exclusions>
</dependency>
- 利用
ffmpeg.addArgumen
添加ffmpeg参数
package com.ffm.test.util;
// 各种excepting 可自定义,或者直接改成 Exception
import com.mobvoi.ffm.common.exception.BaseException;
import com.mobvoi.ffm.common.exception.CombineException;
import com.mobvoi.ffm.common.exception.FrameRateException;
import com.mobvoi.ffm.common.exception.PicException;
import com.mobvoi.ffm.common.model.dto.SourceDetailInfoDto;
import com.mobvoi.ffm.common.resp.ResultCode;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.EncoderException;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;
/**
* 核心工具包
*
* @author shuishan
* @since 2021/7/22
*/
@Slf4j
public class FfmpegUtil {
/**
* 获取音视频时长
*
* @param sourcePath
* @return
* @throws EncoderException
*/
public static long getFileDuration(String sourcePath) throws EncoderException {
MultimediaObject multimediaObject = new MultimediaObject(new File(sourcePath));
MultimediaInfo multimediaInfo = multimediaObject.getInfo();
return multimediaInfo.getDuration();
}
/**
* 剪切音视频
*
* @param sourcePath
* @param targetPath
* @param offetTime 起始时间,格式 00:00:00.000 小时:分:秒.毫秒
* @param endTime 同上
* @throws Exception
*/
public static void cutAv(String sourcePath, String targetPath, String offetTime, String endTime) {
try {
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-ss");
ffmpeg.addArgument(offetTime);
ffmpeg.addArgument("-t");
ffmpeg.addArgument(endTime);
ffmpeg.addArgument("-i");
ffmpeg.addArgument(sourcePath);
ffmpeg.addArgument("-vcodec");
ffmpeg.addArgument("copy");
ffmpeg.addArgument("-acodec");
ffmpeg.addArgument("copy");
ffmpeg.addArgument(targetPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("切除视频成功={}", targetPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "剪切视频失败", null);
}
}
/**
* 等待命令执行成功,退出
*
* @param br
* @throws IOException
*/
private static void blockFfmpeg(BufferedReader br) throws IOException {
String line;
// 该方法阻塞线程,直至合成成功
while ((line = br.readLine()) != null) {
doNothing(line);
}
}
/**
* 打印日志,调试阶段可解开注释,观察执行情况
*
* @param line
*/
private static void doNothing(String line) {
// log.info(line);
}
/**
* 合并两个视频
*
* @param firstFragmentPath 资源本地路径或者url
* @param secondFragmentPath 资源本地路径或者url**
* @param targetPath 目标存储位置
* @throws Exception
*/
public static void mergeAv(String firstFragmentPath, String secondFragmentPath,
String targetPath) {
try {
log.info("合并视频处理中firstFragmentPath={},secondFragmentPath={},请稍后.....", firstFragmentPath,
secondFragmentPath);
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(firstFragmentPath);
ffmpeg.addArgument("-i");
ffmpeg.addArgument(secondFragmentPath);
ffmpeg.addArgument("-filter_complex");
ffmpeg.addArgument(
"\"[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]\" -map \"[v]\" -map \"[a]\"");
ffmpeg.addArgument(targetPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("合并视频成功={}", targetPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "合并视频失败", null);
}
}
/**
* 获取视频原声
*
* @param sourcePath 本地路径或者url
* @param targetPath 本地存储路径
*/
public static String getAudio(String sourcePath, String targetPath, String taskId) {
try {
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(sourcePath);
ffmpeg.addArgument("-f");
ffmpeg.addArgument("mp3");
ffmpeg.addArgument("-vn");
ffmpeg.addArgument(targetPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("获取视频音频={}", targetPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "获取视频音频失败", taskId);
}
return targetPath;
}
/**
* 合并音频
*
* @param originAudioPath 音频url或本地路径
* @param magicAudioPath 音频url或本地路径
* @param audioTargetPath 目标存储本地路径
*/
public static void megerAudioAudio(String originAudioPath, String magicAudioPath,
String audioTargetPath, String taskId) {
try {
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(originAudioPath);
ffmpeg.addArgument("-i");
ffmpeg.addArgument(magicAudioPath);
ffmpeg.addArgument("-filter_complex");
ffmpeg.addArgument("amix=inputs=2:duration=first:dropout_transition=2");
ffmpeg.addArgument("-f");
ffmpeg.addArgument("mp3");
ffmpeg.addArgument("-y");
ffmpeg.addArgument(audioTargetPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("合并音频={}", audioTargetPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "合并音频失败", taskId);
}
}
/**
* 视频加声音
*
* @param videoPath 视频
* @param megerAudioPath 音频
* @param videoTargetPath 目标地址
* @param taskId 可忽略,自行删除taskid
* @throws Exception
*/
public static void mergeVideoAndAudio(String videoPath, String megerAudioPath,
String videoTargetPath, String taskId) {
try {
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(videoPath);
ffmpeg.addArgument("-i");
ffmpeg.addArgument(megerAudioPath);
ffmpeg.addArgument("-codec");
ffmpeg.addArgument("copy");
ffmpeg.addArgument("-shortest");
ffmpeg.addArgument(videoTargetPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("获取视频(去除音频)={}", videoTargetPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "获取视频(去除音频)失败", taskId);
}
}
/**
* 视频增加字幕
*
* @param videoPath
* @param sutitleVideoSavePath
* @param wordPath 固定格式的srt文件地址或存储位置,百度即可
* @return
* @throws Exception
*/
public static boolean addSubtitle(String videoPath, String sutitleVideoSavePath,
String wordPath, String taskId) {
try {
log.info("开始合成字幕....");
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(videoPath);
ffmpeg.addArgument("-i");
ffmpeg.addArgument(wordPath);
ffmpeg.addArgument("-c");
ffmpeg.addArgument("copy");
ffmpeg.addArgument(sutitleVideoSavePath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("添加字幕成功={}", videoPath);
} catch (IOException e) {
throw new CombineException(ResultCode.ERROR.getCode(), "添加字幕失败", taskId);
}
return true;
}
/**
* 图片生成视频 帧率设置25,可自行修改
*
* @param videoPath
* @param videoPath
* @return
* @throws Exception
*/
public static boolean picToVideo(String picsPath, String videoPath, String taskId) {
try {
log.info("图片转视频中....");
ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(picsPath);
ffmpeg.addArgument("-r");
ffmpeg.addArgument("25");
ffmpeg.addArgument("-y");
ffmpeg.addArgument(videoPath);
ffmpeg.execute();
try (BufferedReader br = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
blockFfmpeg(br);
}
log.info("图片转视频成功={}", videoPath);
} catch (IOException e) {
log.error("图片转视频失败={}", e.getMessage());
throw new PicException(ResultCode.ERROR.getCode(), "图片转视频失败", taskId);
}
return true;
}
/**
* 获取视频信息
*
* @param url
* @return
*/
public static MultimediaInfo getVideoInfo(URL url) {
try {
MultimediaObject multimediaObject = new MultimediaObject(url);
return multimediaObject.getInfo();
} catch (EncoderException e) {
log.error("获取视频信息报错={}", e.getMessage());
throw new BaseException(ResultCode.ERROR.getCode(), "获取视频信息报错");
}
}
方法二
拼接底层命令执行
public static String runCmd(String command) {
StringBuilder sb =new StringBuilder();
try {
Process process=Runtime.getRuntime().exec("ffmpeg -i \"input.mp4\" -c:v copy -c:a copy -y -hide_banner \"output.mp4\"");
InputStream inputStream = process.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader=new BufferedReader(inputStreamReader);
String line;
while((line=bufferedReader.readLine())!=null)
{
sb.append(line+"\n");
}
} catch (Exception e) {
return null;
}
return sb.toString();
}