1.视频处理服务
package com.caige.openai.service.impl;
import cn.hutool.core.collection.collectionutil;
import cn.hutool.core.util.strutil;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.stereotype.component;
import org.springframework.util.stopwatch;
import org.springframework.web.multipart.multipartfile;
import java.io.bufferedreader;
import java.io.file;
import java.io.ioexception;
import java.io.inputstreamreader;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.util.arraylist;
import java.util.arrays;
import java.util.list;
import java.util.uuid;
import java.util.stream.collectors;
import java.util.stream.intstream;
import java.util.zip.zipentry;
import java.util.zip.zipoutputstream;
@component
public class videoprocessor {
private static final logger log = loggerfactory.getlogger(videoprocessor.class);
/**
* 根据不同系统创建空路径
*/
private static final file dev_null =
system.getproperty("os.name", "").tolowercase().startswith("win")
? new file("nul")
: new file("/dev/null");
public path savetotempfile(multipartfile file) throws ioexception {
string ext = getfileextension(file.getoriginalfilename());
path tempfile = files.createtempfile("upload_", "." + ext);
files.write(tempfile, file.getbytes());
log.debug("文件临时路径==={}", tempfile.toabsolutepath());
return tempfile;
}
// 获取文件扩展名
private string getfileextension(string filename) {
if (strutil.isblank(filename)) {
return "tmp";
}
int dotindex = filename.lastindexof('.');
return (dotindex == -1) ? "tmp" : filename.substring(dotindex + 1).tolowercase();
}
/**
* 执行ffmpeg命令
*
* @param command 指令集合
* @throws ioexception
* @throws interruptedexception
*/
private void runffmpeg(list<string> command) throws ioexception, interruptedexception {
processbuilder pb = new processbuilder(command);
// 合并 stderr 和 stdout
pb.redirecterrorstream(true);
pb.redirectinput(dev_null);
process process = pb.start();
int exitcode = process.waitfor();
if (exitcode != 0) {
log.error("ffmpeg命令执行失败,退出码==={}", exitcode);
throw new runtimeexception("ffmpeg 命令执行失败,退出码: " + exitcode);
}
}
/**
* 获取视频分辨率,返回 [width, height]
*/
public static int[] getvideoresolution(path videopath) throws ioexception, interruptedexception {
list<string> command = arrays.aslist(
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=s=,:p=0",
videopath.toabsolutepath().tostring()
);
processbuilder pb = new processbuilder(command);
process process = pb.start();
try (bufferedreader reader = new bufferedreader(
new inputstreamreader(process.getinputstream()))) {
string line = reader.readline();
if (line == null) {
throw new runtimeexception("无法读取视频分辨率");
}
// 示例输出: "1920,1080"
string[] wh = line.split(",");
if (wh.length != 2) {
throw new runtimeexception("解析分辨率失败: " + line);
}
int width = integer.parseint(wh[0].trim());
int height = integer.parseint(wh[1].trim());
return new int[]{width, height};
} finally {
process.waitfor();
}
}
public double getvideodurationseconds(path videopath) throws ioexception, interruptedexception {
list<string> command = new arraylist<>(8);
command.add("ffprobe");
command.add("-v");
command.add("error"); // 只输出错误,抑制其他日志
command.add("-show_entries");
command.add("format=duration");
command.add("-of");
command.add("default=nw=1"); // 输出纯数字,如 62.345
command.add(videopath.toabsolutepath().tostring());
processbuilder pb = new processbuilder(command);
process process = pb.start();
try (bufferedreader reader = new bufferedreader(new inputstreamreader(process.getinputstream()))) {
string line = reader.readline();
if (strutil.isblank(line)) {
throw new runtimeexception("无法获取视频时长");
}
// 解析 "duration=32.949002" → 提取 "32.949002"
string trimmed = line.trim();
if (trimmed.startswith("duration=")) {
return double.parsedouble(trimmed.substring("duration=".length()));
} else {
// 兜底:尝试直接解析(兼容未来格式变化)
return double.parsedouble(trimmed);
}
} finally {
process.waitfor();
}
}
private list<integer> calcsnapshottimes(double totalseconds, int intervalseconds) {
if (intervalseconds <= 0) {
intervalseconds = 1;
}
int finalintervalseconds = intervalseconds;
return intstream.iterate(0, t -> t <= totalseconds, t -> t + finalintervalseconds)
.boxed()
.collect(collectors.tolist());
}
private static path creattmpoutputpath(string name) {
return paths.get(system.getproperty("java.io.tmpdir"), name);
}
public list<path> takesnapshot(path videopath, int second) throws ioexception, interruptedexception {
log.debug("当前操作系统==={}", system.getproperty("os.name", "").tolowercase());
stopwatch stopwatch = new stopwatch();
stopwatch.start("视频截图完成耗时");
double duration = getvideodurationseconds(videopath);
string videoabspath = videopath.toabsolutepath().tostring();
log.debug("视频总时长: {} 秒,截图间隔: {} 秒", duration, second);
list<integer> times = calcsnapshottimes(duration, second);
log.debug("将截图时间点: {}", times);
list<path> paths = new arraylist<>();
for (int i = 0; i < times.size(); i++) {
integer time = times.get(i);
string name = "snapshot_" + time + "_" + i + ".jpg";
path outputpath = creattmpoutputpath(name);
string outpath = outputpath.toabsolutepath().tostring();
list<string> cmd = new arraylist<>(11);
cmd.add("ffmpeg");
cmd.add("-ss");
cmd.add(string.valueof(time));
cmd.add("-i");
cmd.add(videoabspath);
cmd.add("-vframes");
cmd.add("1");
cmd.add("-q:v");
cmd.add("2");
cmd.add(outpath);
cmd.add("-y");
runffmpeg(cmd);
paths.add(outputpath);
}
stopwatch.stop();
long millis = stopwatch.gettotaltimemillis();
string seconds = string.format("%.2f", millis / 1000.0);
log.debug("{}==={}ms,{}s,数量==={}", stopwatch.lasttaskinfo().gettaskname(), millis, seconds, paths.size());
return paths;
}
public path addwatermark(path videopath, path watermarkpath) throws ioexception, interruptedexception {
stopwatch stopwatch = new stopwatch();
stopwatch.start("视频添加水印完成耗时");
// 1. 获取视频分辨率
int[] resolution = getvideoresolution(videopath);
int videowidth = resolution[0];
// 2. 计算水印目标宽度(例如占视频宽度的 15%), 最小 100px,避免太小看不清, 最大 400px,防止超大屏下水印过大
int wmtargetwidth = math.max(100, (int) (videowidth * 0.15));
wmtargetwidth = math.min(wmtargetwidth, 400);
// 3. 构建 filter:先缩放水印,再叠加到右上角
string filtercomplex = string.format(
"[1:v]scale=%d:-1[wm];[0:v][wm]overlay=main_w-overlay_w-10:10",
wmtargetwidth
);
string outputfilename = "watermarked_" + uuid.randomuuid() + ".mp4";
path outputpath = creattmpoutputpath(outputfilename);
string outpath = outputpath.toabsolutepath().tostring();
list<string> command = new arraylist<>(13);
command.add("ffmpeg");
command.add("-i");
command.add(videopath.toabsolutepath().tostring());
command.add("-i");
command.add(watermarkpath.toabsolutepath().tostring());
command.add("-filter_complex");
command.add(filtercomplex); // 右上角,距右10px,距上10px
command.add("-c:a");
command.add("aac"); // 确保音频兼容(某些格式需重编码)
command.add("-strict");
command.add("-2");
command.add(outpath);
command.add("-y");
runffmpeg(command);
stopwatch.stop();
long millis = stopwatch.gettotaltimemillis();
string seconds = string.format("%.2f", millis / 1000.0);
log.debug("{}==={}ms,{}s,带水印视频路径==={}", stopwatch.lasttaskinfo().gettaskname(), millis, seconds, outpath);
return outputpath;
}
public path cutvideo(path videopath, int startsecond, int duration) throws ioexception, interruptedexception {
stopwatch stopwatch = new stopwatch();
stopwatch.start("视频剪辑完成耗时");
string outputfilename = "cut_" + uuid.randomuuid() + ".mp4";
path outputpath = creattmpoutputpath(outputfilename);
string outpath = outputpath.toabsolutepath().tostring();
// 计算视频总时长
double durationseconds = getvideodurationseconds(videopath);
log.debug("当前视频总时长==={}s,截取视频时长==={}s", durationseconds, duration - startsecond);
list<string> command = new arraylist<>(17);
command.add("ffmpeg");
command.add("-ss");
command.add(string.valueof(startsecond));
command.add("-i");
command.add(videopath.toabsolutepath().tostring());
command.add("-t");
command.add(duration >= durationseconds ? string.valueof(durationseconds) : string.valueof(duration));
command.add("-c:v");
command.add("libx264");
command.add("-c:a");
command.add("aac");
command.add("-strict");
command.add("-2");
command.add("-preset");
command.add("fast"); // 编码速度 vs 质量
command.add(outpath);
command.add("-y");
runffmpeg(command);
stopwatch.stop();
long millis = stopwatch.gettotaltimemillis();
string seconds = string.format("%.2f", millis / 1000.0);
log.debug("{}==={}ms,{}s,文件路径==={}", stopwatch.lasttaskinfo().gettaskname(), millis, seconds, outpath);
return outputpath;
}
public void buildzip(path zippath, list<path> snapshotpaths) {
if (collectionutil.isempty(snapshotpaths)) {
return;
}
try {
try (zipoutputstream zos = new zipoutputstream(files.newoutputstream(zippath))) {
for (path img : snapshotpaths) {
zipentry entry = new zipentry(img.getfilename().tostring());
zos.putnextentry(entry);
files.copy(img, zos);
zos.closeentry();
}
}
} catch (ioexception e) {
throw new runtimeexception(e);
}
}
}
2.视频处理控制器
package com.caige.openai.contoller;
import cn.hutool.core.io.fileutil;
import cn.hutool.core.util.objectutil;
import cn.hutool.core.util.strutil;
import cn.hutool.core.util.urlutil;
import com.caige.openai.service.impl.videoprocessor;
import jakarta.annotation.resource;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.core.io.filesystemresource;
import org.springframework.http.httpheaders;
import org.springframework.http.mediatype;
import org.springframework.http.responseentity;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.requestparam;
import org.springframework.web.bind.annotation.restcontroller;
import org.springframework.web.multipart.multipartfile;
import java.io.ioexception;
import java.nio.charset.standardcharsets;
import java.nio.file.files;
import java.nio.file.path;
import java.util.arraylist;
import java.util.arrays;
import java.util.list;
@restcontroller
@requestmapping("/video")
public class videocontroller {
private static final logger log = loggerfactory.getlogger(videocontroller.class);
@resource
videoprocessor videoprocessor;
/**
* 视频生成帧截图
*
* @param video 视频文件
* @param second 间隔秒数
* @return {@link responseentity<filesystemresource>}
*/
@postmapping(value = "/snapshot", produces = mediatype.application_octet_stream_value)
public responseentity<filesystemresource> snapshot(
@requestparam("video") multipartfile video,
@requestparam(name = "second", defaultvalue = "5") int second) {
log.debug("获取截图参数=={}, 时间=={}", video, second);
list<path> pathlist = new arraylist<>();
try {
path videopath = videoprocessor.savetotempfile(video);
pathlist = videoprocessor.takesnapshot(videopath, second);
path zippath = files.createtempfile("auto_snapshots_", ".zip");
log.debug("生成zip路径==={}", zippath.toabsolutepath());
videoprocessor.buildzip(zippath, pathlist);
pathlist.add(videopath);
pathlist.add(zippath);
return buildsafedisposition(zippath);
} catch (ioexception | interruptedexception e) {
throw new runtimeexception(e);
}
}
/**
* 视频添加水印
*
* @param video 视频文件
* @param watermark 图片文件,必须是png格式
* @return {@link responseentity<filesystemresource>}
*/
@postmapping(value = "/watermark", produces = mediatype.application_octet_stream_value)
public responseentity<filesystemresource> addwatermark(
@requestparam("video") multipartfile video,
@requestparam("watermark") multipartfile watermark) {
list<path> pathlist = new arraylist<>();
try {
if (objectutil.hasnull(video, watermark)) {
throw new runtimeexception("视频文件与水印png图片不能为空");
}
string extname = fileutil.extname(watermark.getoriginalfilename());
if (!strutil.equalsignorecase(extname, "png")) {
throw new runtimeexception("水印图片必须为png格式");
}
// 通过 mime 类型验证是否真的是 png 图片
if (!strutil.equalsignorecase(watermark.getcontenttype(), "image/png")) {
throw new runtimeexception("上传的文件mime类型不匹配,必须是image/png");
}
path videopath = videoprocessor.savetotempfile(video);
path wmpath = videoprocessor.savetotempfile(watermark);
path resultpath = videoprocessor.addwatermark(videopath, wmpath);
pathlist.add(videopath);
pathlist.add(wmpath);
pathlist.add(resultpath);
return buildsafedisposition(resultpath);
} catch (ioexception | interruptedexception e) {
throw new runtimeexception(e);
}
}
/**
* 视频截取
*
* @param video 视频文件
* @param start 开始时间——秒
* @param duration 截止时间——秒
* @return {@link responseentity<filesystemresource>}
*/
@postmapping(value = "/cut", produces = mediatype.application_octet_stream_value)
public responseentity<filesystemresource> cutvideo(
@requestparam("video") multipartfile video,
@requestparam("start") int start,
@requestparam("duration") int duration) {
if (duration < 5) {
throw new illegalargumentexception("剪辑时长必须大于5秒");
}
path videopath = null;
path resultpath = null;
try {
videopath = videoprocessor.savetotempfile(video);
resultpath = videoprocessor.cutvideo(videopath, start, duration);
return buildsafedisposition(resultpath);
} catch (ioexception | interruptedexception e) {
throw new runtimeexception(e);
}
}
private responseentity<filesystemresource> buildsafedisposition(path path) throws ioexception {
string encodedname = urlutil.encode(string.valueof(path.getfilename()), standardcharsets.utf_8);
string disposition = string.format("attachment; filename=\"%s\"; filename*=utf-8''%s",
encodedname,
encodedname);
filesystemresource resource = new filesystemresource(path.tofile());
return responseentity.ok()
.header(httpheaders.content_length, string.valueof(resource.contentlength()))
.header(httpheaders.content_disposition, disposition)
.body(resource);
}
}
3.maven依赖
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter</artifactid>
<version>3.4.3</version>
</dependency>
<dependency>
<groupid>org.springframework</groupid>
<artifactid>spring-web</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
<version>3.4.3</version>
</dependency>
<dependency>
<groupid>org.springframework</groupid>
<artifactid>spring-context</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-configuration-processor</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-autoconfigure</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-aop</artifactid>
</dependency>
<dependency>
<groupid>jakarta.servlet</groupid>
<artifactid>jakarta.servlet-api</artifactid>
<version>6.0.0</version>
</dependency>
<dependency>
<groupid>jakarta.annotation</groupid>
<artifactid>jakarta.annotation-api</artifactid>
<version>2.1.1</version>
</dependency>
<dependency>
<groupid>cn.hutool</groupid>
<artifactid>hutool-all</artifactid>
<version>5.8.36</version>
</dependency>4.其他事项
注意:上述代码依赖 ffmpeg 工具,请先确保它已在您的系统中安装并配置好环境变量。如果您尚未安装,可参考官方文档或相关教程完成安装,本文不再详细介绍。
以上就是springboot调用ffmpeg实现对视频的截图、截取与水印的详细内容,更多关于springboot ffmpeg视频的截图、截取与水印的资料请关注代码网其它相关文章!
发表评论