9.1 KiB
9.1 KiB
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();
}