springboot+ffmpeg进行图片、音频、视频处理

前言

最近项目有需求,对图片、音频、视频进行一些简单处理,简单学习后了解到ffmpeg可以实现这些功能,这里对使用到的部分做说明。功能需要安装ffmpeg环境。

1. ffmpeg处理命令

这里介绍相关操作的ffmpeg处理命令。

1.1 图片处理

1.1.1 图片翻转

图片翻转包括水平翻转和垂直翻转。

# 原文件 source.jpg 目标文件 target.jpg
# 水平翻转
ffmpeg -i /data/image/source.jpg -vf hflip /data/image/target.jpg
# 垂直翻转
ffmpeg -i /data/image/source.jpg -vf vflip /data/image/target.jpg

1.2 音频处理

1.2.1 音频裁剪
# 从00:00:10秒开始裁剪,裁剪10秒,原文件 source.mp3 目标文件 temp1.mp3
ffmpeg -ss 00:00:10 -i source.mp3 10 temp_1.mp3
1.2.2 音频合并

方式一:

# 将temp_1.mp3和temp_2.mp3合并为target.mp3
ffmpeg -i "concat:temp_1.mp3|temp_2.mp3" -acodec copy target.mp3

方式二:

# 将temp_1.mp3和temp_2.mp3合并为target.mp3
# 注意这里的n后面的值要随着前面音频片段的个数改变而改变
ffmpeg -i temp_1.mp3 -i temp_2.mp3 -filter_complex "[0:0][1:0]concat=n=2:v=0:a=1[out]" -map "[out]" target.mp3

1.3 视频处理

1.3.1 视频提取音频
# 将source.mp4 音频提取为 target.mp3
ffmpeg -i source.mp4 -vn target.mp3
1.3.2 视频倍速

注意:
setpts这里指的是将视频变为0.5倍速,视频变为2倍速,这个值对应为0.5,4倍速对应的值为0.25。
atempo指的是音频变为0.5倍速,值和视频对应的值同理,这里如果不加atempo属性,就会导致视频倍速了,音频还是原速。

# 将source.ts 倍速变为0.5倍速
ffmpeg -i source.mp4 -filter_complex "[0:v]setpts=2.0*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" target.mp4
1.3.3 视频裁剪

这里解释一下:
-to 表示截取xx-xx时间,也可以用 00:01:00 -t 00:01:00 表示从第一分钟开始截取一分钟时长
-vcodec copy 表示视频采取复制原视频方式(不重新编码)
-acodec copy 表示音频采取复制原音频方式
-strict -2 表示关闭严格模式
-keyint_min 2 设置最小关键帧间隔为2
-g 1 将每一帧都变味关键帧
-y 覆盖文件(输出路径存在文件就覆盖原文件)

# 将source.mp4视频从00:01:00 - 00:03:00 切割出来为target.mp4
ffmpeg -i source.mp4 -ss 00:01:00 -to 00:03:00 -vcodec copy -acodec copy -strict -2 -keyint_min 2  -g 1  -y target.mp4
1.3.4 视频拼接

方式一:
这里解释一下:
-filter_complex 启用复杂滤镜图,用于拼接多个输入流。
[0:v][0:a] 取第一个视频流,取第一个音频流,后面以此类推
concat=n=3,3表示三个输入
v=1:a=1[outv][outa] 合并视频流音频流,输出视频流音频流
-map “[outv]” -map “[outa]” 选择对应的视频流音频流(刚刚合并输出的)

ffmpeg -i temp_1.mp4 -i temp_2.mp4 -i temp_3.mp4 -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" target.mp4

方式二:
-f concat 表示使用 concat 协议
-safe 0 用于允许读取相对路径的文件
-i filelist.txt 指定包含视频文件列表的文件
-c copy 表示拷贝原始视频和音频流,而不重新编码

# 合并file.txt文件中的视频文件为output.mp4
ffmpeg -f concat -safe 0 -i file.txt -c copy output.mp4

file内容如下:

file 'temp_1.mp4'
file 'temp_2.mp4'
file 'temp_3.mp4'
1.3.5 视频添加字幕

这里需要准备一个字幕文件subtitle.srt,文件内容如下

1
00:00:00,000 --> 00:00:05,000
第一段字幕

2
00:00:05,001 --> 00:00:10,000
第二段字幕

3
00:00:10,001 --> 00:00:15,000
第三段字幕

字幕文件中,00:00:10,001 表示 时分秒毫秒

ffmpeg -i input.mp4 -vf "subtitles=subtitle.srt" -c:a copy output.mp4
1.3.6 视频添加水印

1.添加图片水印

ffmpeg -i source.mp4 -i image.jpg -filter_complex "overlay=W-w-10:H-h-10" target.mp4 

2.添加文字水印

# fontsize 指定文字大小,fontfile指定字体文件,fontcolor指定字体颜色 x,y指定水印位置
ffmpeg -i source.mp4 -vf drawtext=text='文字水印:fontsize=24:fontfile=/data/font/Arial:fontcolor=white:x=10:y=10' -c:a copy target.mp4

2. java操作ffmpeg

总体操作分为两部,拼接命令 -> 执行命令,这里使用ProcessBuilder来处理执行命令。
这里拿给视频添加文字水印进行举例。代码如下:

public static void main(String[] args) throws Exception {
        String textWater = "文本水印";
        String videoPath = "E:\\video\\test.mp4"; // 原文件
        String targetPath = "E:\\video\\water.mp4"; // 目标文件
        List<String> commandList = new ArrayList<>();
        commandList.add("ffmpeg");
        commandList.add("-i");
        commandList.add(videoPath);
        commandList.add("-vf");
        commandList.add("drawtext=text='" + textWater + "':fontsize=24:fontfile=/data/font/Arial:fontcolor=white:x=10:y=10");
        commandList.add("-c:a");
        commandList.add("copy");
        commandList.add(targetPath);
        StringBuilder sb = new StringBuilder();
        commandList.forEach(str -> sb.append(str).append(" "));
        FFmpegUtils.processCmd(commandList);
        System.out.println("完成");
    }

工具类代码如下:

@Slf4j
public class FFmpegUtils {

	/**
	 * 调用ffmpeg执行对应的ffmpeg命令
	 * @param commandList 命令集合
	 */
	public static boolean processCmd(List<String> commandList) throws Exception {
		ProcessBuilder processBuilder = new ProcessBuilder(commandList);
		processBuilder.redirectErrorStream(true);
		Process process = null;
		process = processBuilder.start();
		InputStream inputStream = process.getInputStream();
		// 打印FFmpeg输出
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
			String line;
			while ((line = reader.readLine()) != null) {
				System.out.println(line);
			}
		}
		// 等待处理完成
		int exitCode = process.waitFor();
		process.destroy();
		if (exitCode == 0) {
			log.info("执行成功");
			return true;
		} else {
			StringBuilder sb = new StringBuilder();
			commandList.forEach(str -> sb.append(str).append(" "));
			log.info("ffmpeg执行命令失败 , 命令 -> {}", sb);
		}
		return false;
	}

    /**
     * 调用ffmpeg执行对应的ffmpeg命令(日志异步输出)
     * 		有时在处理部分指令的时候输出过多导致线程阻塞,就会执行失败,这里就可以将输出日志新建线程打印,防止这种情况
     * @param commandList 命令集合
     */
    public static boolean processCmdAsync(List<String> commandList) throws Exception {
        StringBuilder sb = new StringBuilder();
        commandList.forEach(str -> sb.append(str).append(" "));
        log.info("ffmpeg执行命令 -> {}", sb);
        ProcessBuilder processBuilder = new ProcessBuilder(commandList);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        // 开线程打印避免线程阻塞
        Thread outputThread = new Thread(() -> readStream(process.getInputStream()));
        Thread errorThread = new Thread(() -> readStream(process.getErrorStream()));
        outputThread.start();
        errorThread.start();
        // 等待处理完成
        int exitCode = process.waitFor();
        log.info("命令处理完成 code -> {}", exitCode);
        outputThread.join();
        errorThread.join();
        if (exitCode == 0) {
            log.info("执行成功");
            process.destroy();
            return true;
        }
        process.destroy();
        return false;
    }

    /**
     * 打印ffmpeg输出
     * @param inputStream
     */
    private static void readStream(InputStream inputStream) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里提一下我这踩的坑,当时在做音频处理的时候,对音频进行拼接,使用到的是前面我提到的方式一的音频合并,但是执行命令始终执行失败,排查以为是文件没有问题,是命令问题,但是在执行中,我打印了我的命令,我直接将命令放在项目部署的容器内也能执行,拿到宿主机也能执行,寻求同事援助,他判断原因可能是ProcessBuilder的兼容问题,不支持这种语法,然后就换了音频拼接的方式二,执行就成功了。对应音频拼接的方式一,视频拼接也能采用这种方式,但是由于之前音频的问题,所以这里我也没提到这种方式,有兴趣可以去试一下。

实现视频上传和回显,需要涉及前端页面、后端接口以及视频处理等多方面的技术。下面我将从这些方面一步一步介绍具体实现方法。 1. 前端页面 前端页面需要有一个上传视频的功能,可以使用`<input type="file">`标签实现。在上传时,可以通过`FormData`对象将视频文件和一些其他参数一起发送到后端接口。示例代码如下: ```html <template> <div> <input type="file" @change="handleFileUpload"> <button @click="uploadVideo">上传</button> <video v-if="videoUrl" :src="videoUrl" controls></video> </div> </template> <script> export default { data() { return { videoUrl: '' } }, methods: { handleFileUpload(event) { this.videoFile = event.target.files[0] }, async uploadVideo() { const formData = new FormData() formData.append('file', this.videoFile) formData.append('name', 'video') const response = await fetch('/api/upload', { method: 'POST', body: formData }) const data = await response.json() if (data.success) { this.videoUrl = data.videoUrl } } } } </script> ``` 2. 后端接口 后端接口使用SpringBoot框架,需要使用`@RestController`和`@PostMapping`注解来实现视频上传接口。接收到视频文件后,可以使用FFmpeg库来处理视频文件,将视频转换为指定格式或者提取视频的缩略图等。 ```java @RestController public class VideoController { @PostMapping("/api/upload") public ApiResponse uploadVideo(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return ApiResponse.error("上传文件不能为空"); } try { // 保存视频文件并返回视频的URL String videoUrl = saveVideo(file); return ApiResponse.success(videoUrl); } catch (Exception e) { e.printStackTrace(); return ApiResponse.error("上传失败"); } } private String saveVideo(MultipartFile file) throws Exception { String fileName = UUID.randomUUID().toString() + ".mp4"; File dest = new File("uploads/" + fileName); file.transferTo(dest); return "http://localhost:8080/uploads/" + fileName; } } ``` 3. 视频处理 视频处理需要使用FFmpeg库来实现。在SpringBoot项目中,可以使用`ProcessBuilder`来执行FFmpeg命令。下面是一个实现视频转换为MP4格式的示例代码: ```java private void convertToMp4(String inputPath, String outputPath) throws Exception { String command = String.format("ffmpeg -i %s -c:v libx264 -preset slow -crf 22 -c:a aac -b:a 128k -movflags faststart -f mp4 %s", inputPath, outputPath); Process process = new ProcessBuilder(command.split(" ")).redirectErrorStream(true).start(); InputStream inputStream = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } process.waitFor(); reader.close(); inputStream.close(); } ``` 以上是实现视频上传和回显的基本步骤,具体实现还需要根据具体需求进行调整。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值