当前位置: 代码网 > it编程>编程语言>Java > SpringBoot调用ffmpeg实现对视频的截图、截取与水印

SpringBoot调用ffmpeg实现对视频的截图、截取与水印

2026年01月18日 Java 我要评论
1.视频处理服务package com.caige.openai.service.impl;import cn.hutool.core.collection.collectionutil;import

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视频的截图、截取与水印的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com