MinIO中视频转换为HLS协议并进行AES加密
一、视频切片进行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;@Autowiredprivate FileService fileService;@Resourceprivate FileConfigService fileConfigService;@Resourceprivate FFmpegPathResolver ffmpegPathResolver;@Asyncpublic 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. 上传切片文件到MinIOuploadHlsFiles(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, Uif (e.ctrlKey && forbiddenKeys.includes(e.keyCode)) {e.preventDefault();alert('禁止操作');}
}
</script>