一、导入pom依赖
<profiles>
<profile>
<id>local</id>
<properties>
<profiles.active>local</profiles.active>
<!--根据环境进行切换-->
<ffmpeg.classifier>windows-x86_64</ffmpeg.classifier>
<!-- <ffmpeg.classifier>linux-x86_64</ffmpeg.classifier>-->
</properties>
</profile>
</profiles>
<!-- javacv+javacpp核心库-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacv.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp-platform</artifactId>
<version>${javacv.version}</version>
</dependency>
<!-- ffmpeg最小依赖包,必须包含上面的javacv+javacpp核心库 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>5.1.2-${javacv.version}</version>
<classifier>${ffmpeg.classifier}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${javacv.version}</version>
<classifier>${ffmpeg.classifier}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>0.3.21-${javacv.version}</version>
<classifier>${ffmpeg.classifier}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0-${javacv.version}</version>
<classifier>${ffmpeg.classifier}</classifier>
</dependency>
二、预览图生成代码
package com.czi.dcp.biz.eventquickprocess.service;
import cn.hutool.core.img.gif.AnimatedGifEncoder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.UUID;
/**
* 视频缩略图处理器
*
* @author god_cvz
*/
@Component
@Slf4j
public class VideoPreviewProcessor {
public void asyncProcess(String videoUrl) {
new Thread(() -> process(videoUrl)).start();
}
public void process(String videoUrl) {
log.info("开始处理视频缩略图 videoUrl: {}", videoUrl);
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoUrl)) {
grabber.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
// 生成webp,动图
generateWebp(grabber, converter);
// 生成缩略图
generateThumbnails(grabber, converter);
} catch (Exception | Error e) {
log.error("处理视频缩略图失败: {}", e.getMessage());
}
}
private void generateThumbnails(FFmpegFrameGrabber grabber, Java2DFrameConverter converter) {
// 缩略图宽度
int thumbnailWith = 200;
// 缩略图高度
int thumbnailHeight = 150;
// gif动图时长
int gifDuration = 10;
// gif动图质量
int gifQuality = 10;
String thumbnailsName = UUID.randomUUID() + ".gif";
String thumbnailsObjectKey = "/bucket/gif" + thumbnailsName;
uploadThumbnails(grabber, converter, thumbnailWith, thumbnailHeight, gifDuration, gifQuality, thumbnailsObjectKey);
}
private void uploadThumbnails(FFmpegFrameGrabber grabber, Java2DFrameConverter converter,
Integer targetWidth, Integer targetHeight,
Integer gifDuration, Integer gifQuality,
String objectKey) {
ByteArrayInputStream inputStream = null;
try (ByteArrayOutputStream gifOutPutStream = new ByteArrayOutputStream()) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
grabber.setFrameNumber(0);
int targetFrameCount = (int) Math.min(gifDuration * grabber.getFrameRate(), grabber.getLengthInFrames());
// 设置缩放比例
int[] targetSize = getChangeSize(grabber.getImageWidth(), grabber.getImageHeight(), targetWidth, targetHeight);
// 设置gif图像宽高
grabber.setImageWidth(targetSize[0]);
grabber.setImageHeight(targetSize[1]);
// 使用AnimatedGifEncoder转换视频流
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
encoder.start(gifOutPutStream);
encoder.setDelay((int) (1000 / grabber.getFrameRate()));
encoder.setFrameRate((float) grabber.getFrameRate());
encoder.setQuality(gifQuality);
encoder.setRepeat(0);
for (int i = 0; i < targetFrameCount; i++) {
Frame frame = grabber.grabImage();
BufferedImage image = converter.convert(frame);
encoder.addFrame(image);
}
encoder.finish();
stopWatch.stop();
inputStream = new ByteArrayInputStream(gifOutPutStream.toByteArray());
// 上传oss
this.uploadFile(objectKey, inputStream);
log.info("生成缩略图成功: objectKey: {}, 处理耗时: {}s", objectKey, stopWatch.getTotalTimeSeconds());
} catch (Exception e) {
log.error("生成缩略图失败: objectKey: {} - 原因:{}", objectKey, e.getMessage());
} finally {
IoUtil.closeIfPosible(inputStream);
}
}
private void generateWebp(FFmpegFrameGrabber grabber, Java2DFrameConverter converter) {
// webp缩略图宽度
int webpWidth = 720;
// webp缩略图高度
int webpHeight = 540;
// gif动图时长
int gifDuration = 10;
// gif动图质量
int gifQuality = 10;
String webpFileName = UUID.randomUUID() + ".webp";
String webpObjectKey = "/bucket/webp/" + webpFileName;
uploadWebp(grabber, webpWidth, webpHeight, gifDuration, gifQuality, webpObjectKey, webpFileName);
}
private void uploadWebp(FFmpegFrameGrabber grabber, Integer targetWidth, Integer targetHeight, Integer gifDuration, Integer gifQuality, String objectKey, String filename) {
File outputFile = new File(filename);
FileInputStream fileInputStream = null;
try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, targetWidth, targetHeight)) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
grabber.setFrameNumber(0);
int targetFrameCount = (int) Math.min(gifDuration * grabber.getFrameRate(), grabber.getLengthInFrames());
// 设置缩放比例
int[] targetSize = getChangeSize(grabber.getImageWidth(), grabber.getImageHeight(), targetWidth, targetHeight);
// 设置gif图像宽高
grabber.setImageWidth(targetSize[0]);
grabber.setImageHeight(targetSize[1]);
recorder.setFormat("webp");
recorder.setVideoCodec(avcodec.AV_CODEC_ID_WEBP);
recorder.setOption("loop", "0");
recorder.setVideoQuality(gifQuality);
recorder.setFrameRate(grabber.getFrameRate());
recorder.start();
for (int i = 0; i < targetFrameCount; i++) {
Frame frame = grabber.grabImage();
recorder.record(frame);
}
recorder.close();
stopWatch.stop();
fileInputStream = new FileInputStream(outputFile);
// Webp动图文件 上传云盘
this.uploadFile(objectKey, fileInputStream);
log.info("生成webp成功: objectKey: {}, 处理耗时: {}s", objectKey, stopWatch.getTotalTimeSeconds());
} catch (Exception e) {
log.error("生成webp失败: objectKey: {} - 原因: {}", objectKey, e.getMessage());
} finally {
IoUtil.closeIfPosible(fileInputStream);
FileUtil.del(outputFile);
}
}
/**
* 计算图像 按比例缩放后的宽高
* 选择宽高缩放后较大的一边,固定其长度,另一边按原图比例缩放
*
* @param originalWidth 图像原宽度
* @param originalHeight 图像原长度
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @return int[]
*/
public static int[] getChangeSize(int originalWidth, int originalHeight, int targetWidth, int targetHeight) {
// 设置缩放比例
double scale = Math.min(targetWidth / (double) originalWidth, targetHeight / (double) originalHeight);
// 计算缩放后的宽高
return new int[]{(int) (originalWidth * scale), (int) (originalHeight * scale)};
}
/**
* todo:自定义上传
*
* @param objectKey OSS存储key
* @param inputStream 流数据
*/
private void uploadFile(String objectKey, InputStream inputStream) {
}
}
tips:
1.建议使用线程池进行异步处理
2.代码中可自行修改预览图缩略图宽高
3.文件上传逻辑各自处理