一、视频切片进行AES加密
win10下载FFmpeg D:\ProgramFiles\ffmpeg
把ffmpeg的绝对路径配置进来
package cn.teaching.jlk.module.infra.framework.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class FFmpegPathResolver {
@Value("${ffmpeg.path.linux:/usr/bin/ffmpeg}")
private String linuxPath;
@Value("${ffmpeg.path.win:D:/ffmpeg/bin/ffmpeg.exe}")
private String windowsPath;
public String getFFmpegPath() {
return System.getProperty("os.name").toLowerCase().contains("win")
? windowsPath : linuxPath;
}
}
- 从MinIo下载视频到本地,切片完成之后进行AES加密上传到MinIo
import cn.hutool.core.util.HexUtil;
import cn.teaching.jlk.module.infra.framework.file.core.client.FileClient;
import cn.teaching.jlk.module.infra.framework.utils.FFmpegPathResolver;
import jakarta.annotation.Resource;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.stream.Stream;
// 4. HLS 切片服务 (异步处理)
@Service
@EnableAsync
public class HlsConversionService {
@Value("${hls.segment-duration}")
private int segmentDuration;
@Autowired
private FileService fileService;
@Resource
private FileConfigService fileConfigService;
@Resource
private FFmpegPathResolver ffmpegPathResolver;
@Async
public void convertToHls(String sourceObject, String videoId, String keyUri) {
try {
// 1. 从MinIO下载原始视频到临时文件
Path tempDir = Files.createTempDirectory("hls_conversion");
Path inputFile = tempDir.resolve("source.mp4");
FileClient client = fileConfigService.getMasterFileClient();
byte[] fileContent = fileService.getFileContent(client.getId(), sourceObject);
Files.write(inputFile, fileContent);
// 2. 创建输出目录
Path outputDir = tempDir.resolve("output");
Files.createDirectories(outputDir);
// 3. 创建密钥文件
Path keyInfoFile = tempDir.resolve("enc.keyinfo");
Path keyFile = tempDir.resolve("enc.key");
String hexKey = generateHexKey();
// 3.1生成 key 文件
Files.write(keyFile, getKeyBytes());
// 3.2生成 keyinfo 文件
String keyInfoContent = keyUri + "\n" + keyFile.toAbsolutePath().toString() + "\n" + hexKey;
Files.write(keyInfoFile, keyInfoContent.getBytes());
// 4. 执行FFmpeg转换
FFmpeg ffmpeg = new FFmpeg(ffmpegPathResolver.getFFmpegPath());
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(inputFile.toString())
.addOutput(outputDir.resolve("playlist.m3u8").toString())
.setFormat("hls")
.addExtraArgs("-hls_time", String.valueOf(segmentDuration))
.addExtraArgs("-hls_key_info_file", keyInfoFile.toString())
.addExtraArgs("-hls_playlist_type", "vod")
.addExtraArgs("-hls_segment_filename", outputDir.resolve("segment_%03d.ts").toString())
.addExtraArgs("-hls_flags", "independent_segments")
.addExtraArgs("-g", "48")
.addExtraArgs("-sc_threshold", "0")
.addExtraArgs("-c:v", "libx264")
.addExtraArgs("-c:a", "aac")
.addExtraArgs("-b:v", "2000k")
.addExtraArgs("-b:a", "128k")
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg);
executor.createJob(builder).run();
// 5. 上传切片文件到MinIO
uploadHlsFiles(outputDir, videoId);
// 6. 清理临时文件
FileUtils.deleteDirectory(tempDir.toFile());
} catch (Exception e) {
throw new RuntimeException("HLS conversion failed", e);
}
}
/**
* 获取 AES 密钥
* @return
*/
public byte[] getKeyBytes() {
String hexKey = "a1b2c3d4e5f678901234567890abcdef";
return HexUtil.decodeHex(hexKey);
}
private String generateHexKey() {
byte[] key = new byte[16];
new SecureRandom().nextBytes(key);
StringBuilder sb = new StringBuilder();
for (byte b : key) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
}
private void uploadHlsFiles(Path outputDir, String videoId) {
try (Stream<Path> paths = Files.walk(outputDir)) {
paths.filter(Files::isRegularFile)
.forEach(file -> {
String objectName = videoId + "/" + file.getFileName().toString();
try (InputStream is = Files.newInputStream(file)) {
fileService.createFile(file.getFileName().toString(),objectName, is.readAllBytes(), false);
} catch (Exception e) {
throw new RuntimeException("HLS file upload failed", e);
}
});
} catch (Exception e) {
throw new RuntimeException("HLS file upload failed", e);
}
}
}
二、防下载技术组合拳
- MinIO 桶策略
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::video-bucket/*",
"Condition": {
"StringNotLike": {
"aws:Referer": [
"https://your-domain.com/*"
]
}
}
}
]
}
- Nginx 代理加固
location ~ \.(m3u8|ts|key)$ {
proxy_pass http://minio-server;
# 关键防御头
add_header X-Content-Type-Options "nosniff";
add_header Content-Disposition "inline";
add_header Content-Security-Policy "default-src 'self'";
# 限制Range请求(防下载工具)
if ($http_range) {
return 416;
}
}
- 动态水印叠加
// 使用FFmpeg实时叠加用户专属水印
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(inputUrl)
.addOutput(outputUrl)
.addExtraArgs("-vf",
"drawtext=text='User %{userId}': fontcolor=white@0.5: fontsize=24: box=1: boxcolor=black@0.5: x=10: y=10")
.done();
- 禁用右键/快捷键
<template>
<div @contextmenu.prevent @keydown.prevent="blockShortcuts">
<video ref="videoEl" controls></video>
</div>
</template>
<script>
function blockShortcuts(e) {
const forbiddenKeys = [67, 83, 85]; // C, S, U
if (e.ctrlKey && forbiddenKeys.includes(e.keyCode)) {
e.preventDefault();
alert('禁止操作');
}
}
</script>