Files
blog-press/docs/Web-Backend/Others/VideoCompressor.md
2026-01-29 15:01:35 +08:00

9.1 KiB
Raw Blame History

title, date
title date
视频压缩 2026-01-29

一、简介

视频码率Bit Rate指的是视频文件在单位时间内通常是每秒所包含的数据量。单位通常是 Mbps兆比特每秒或 Kbps千比特每秒
  码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
如果不压缩原始视频数据量极其庞大一部2小时的未压缩1080p电影可能占用数TB的存储空间普通硬盘无法承受家庭宽带或移动网络无法实时传输如此巨大的数据量视频播放会不断卡顿。
如果服务器的带宽为10M适合 720p勉强支持低码率 1080p。

二、引入依赖

<!-- Jave 2 视频处理库 -->
<dependency>
    <groupId>ws.schild</groupId>
    <artifactId>jave-all-deps</artifactId>
    <version>3.5.0</version>
</dependency>

三、工具类

import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.Encoder;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.info.VideoSize;

import java.io.File;

/**
 * 视频压缩工具类
 * 提供将视频压缩到720p的静态方法
 */
@Slf4j
public class VideoCompressor {

    // 支持的视频格式
    private static final String[] SUPPORTED_FORMATS = {
            ".mp4", ".avi", ".mov", ".mkv", ".flv",
            ".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
    };

    // 压缩参数常量
    private static final int TARGET_WIDTH = 1280;
    private static final int TARGET_HEIGHT = 720;
    private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
    private static final int AUDIO_BITRATE = 128000;   // 128 kbps
    private static final String VIDEO_CODEC = "libx264";
    private static final String AUDIO_CODEC = "aac";
    private static final String OUTPUT_FORMAT = "mp4";
    private static final String OUTPUT_SUFFIX = "_720p.mp4";

    private static final int MAX_BITRATE = 2500; // 2.5 Mbps
    private static final int MIN_BITRATE = 1000; // 1 Mbps

    /**
     * 压缩视频到720p统一输出为MP4格式
     *
     * @param inputFile 输入视频文件
     * @return 压缩后的视频文件,如果不需要压缩则返回原文件
     * @throws Exception 压缩过程中可能出现的异常
     */
    public static File compressTo720p(File inputFile) throws Exception {
        if (!isSupportedVideoFile(inputFile)) {
            log.info("文件格式不支持或文件不存在,直接返回源文件");
            return inputFile;
        }

        // 判断是否需要压缩
        if (!needCompression(inputFile)) {
            return inputFile;
        }

        File outputFile = createOutputFile(inputFile);
        Encoder encoder = new Encoder();

        // 配置编码参数
        EncodingAttributes encodingAttributes = createEncodingAttributes();

        log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
        encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
        log.info("视频压缩完成: {}", outputFile.getName());

        return outputFile;
    }

    /**
     * 智能判断视频是否需要压缩
     * 基于分辨率、码率、数据密度等多维度综合判断
     */
    public static boolean needCompression(File videoFile) throws Exception {
        MultimediaObject media = new MultimediaObject(videoFile);

        // 获取视频基本信息
        double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
        double duration = media.getInfo().getDuration() / 1000.0;
        int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps

        // 获取视频分辨率
        int width = media.getInfo().getVideo().getSize().getWidth();
        int height = media.getInfo().getVideo().getSize().getHeight();

        log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);

        // 判断逻辑:基于分辨率和码率的智能判断
        boolean needsCompression = false;

        // 1. 如果分辨率已经<=720p检查码率是否过高
        if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
            if (bitRateKbps > MAX_BITRATE) {
                log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
                needsCompression = true;
            } else {
                log.info("视频分辨率已达720p或以下且码率合理无需压缩");
            }
        }
        // 2. 如果分辨率>720p但码率很低说明已是优化过的视频无需压缩
        else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
            log.info("视频虽为{}p高分辨率但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
        }
        // 3. 高分辨率+高码率,需要压缩
        else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
            log.info("视频为{}p高分辨率且码率较高({}kbps)需要压缩至720p", height, bitRateKbps);
            needsCompression = true;
        }
        // 4. 其他情况(如异常数据)默认不压缩
        else {
            log.info("视频参数异常,保守处理:不进行压缩");
        }

        return needsCompression;
    }

    /**
     * 检查文件是否为支持的视频格式
     */
    private static boolean isSupportedVideoFile(File file) {
        if (file == null || !file.exists()) {
            return false;
        }

        String filename = file.getName().toLowerCase();
        for (String format : SUPPORTED_FORMATS) {
            if (filename.endsWith(format)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 创建输出文件对象
     */
    private static File createOutputFile(File inputFile) {
        String baseName = getFileBaseName(inputFile.getName());
        String outputFilename = baseName + OUTPUT_SUFFIX;
        return new File(inputFile.getParent(), outputFilename);
    }

    /**
     * 获取文件名(不含扩展名)
     */
    private static String getFileBaseName(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
    }

    /**
     * 创建编码配置参数
     */
    private static EncodingAttributes createEncodingAttributes() {
        // 音频配置
        AudioAttributes audio = new AudioAttributes();
        audio.setCodec(AUDIO_CODEC);
        audio.setBitRate(AUDIO_BITRATE);

        // 视频配置
        VideoAttributes video = new VideoAttributes();
        video.setCodec(VIDEO_CODEC);
        video.setBitRate(VIDEO_BITRATE);
        video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));

        // 编码设置
        EncodingAttributes attributes = new EncodingAttributes();
        attributes.setOutputFormat(OUTPUT_FORMAT);
        attributes.setAudioAttributes(audio);
        attributes.setVideoAttributes(video);

        return attributes;
    }
}

  通过分辨率和码率的双重判断,确定是否需要压缩。

四、使用

public void uploadFile(MultipartFile file, String filePath) {
    try {
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();

        File tempFile = convertMultipartFileToFile(file);
        log.info("临时文件地址:{}", tempFile.toPath());

        log.info("开始压缩文件:{}", tempFile.getName());
        File compressFile = VideoCompressor.compressTo720p(tempFile);
        log.info("完成压缩文件:{}", tempFile.getName());

        log.info("压缩文件地址:{}", tempFile.toPath());

        body.add("file", new FileSystemResource(compressFile));

        aListClient.put()
                .uri("api/fs/form")
                .header(HttpHeaders.AUTHORIZATION, token)
                .header("File-Path", filePath)
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(body)
                .retrieve()
                .body(Map.class);

        Files.deleteIfExists(compressFile.toPath());
        Files.deleteIfExists(tempFile.toPath());
    } catch (Exception e) {
        log.error("文件上传失败{}", e.getMessage());
        throw new RuntimeException("文件上传失败", e);
    }
}

private File convertMultipartFileToFile(MultipartFile file) throws IOException {
    String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
            ? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
            : ".tmp";

    Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
    file.transferTo(tempFile);
    return tempFile.toFile();
}