前言
本篇文章主要是分享代码,代码实现的功能是从oss上指定bucket下的upload目录下载视频到本地,然后通过openssl生成key与iv和keyinfo文件,然后调用脚本通过ffmpeg指令切分mp4视频到当前目录下,由于是调用脚本所以程序不会等待视频切分完成,会立马往下执行,这时通过获取视频时长计算视频ts切片数量,然后统计目录下ts文件数量,一旦大于或者等于立马停止等待,遍历目录下的ts、m3u8与key文件进行上传到oss中。使用m3u8的原始是为了视频安全,使用oss是为了实现视频的高并发访问。记录一下在生成环境遇到的视频加密问题,不是很清楚加密思路的可以先看lz的这篇文章,了解加密用到的相关技术:Java实现视频加密及播放 之前我是在windows上测试视频切分功能问题不大,但是到了Linux环境发现了很多问题,例如无法通过ProcessBuild(类似于黑窗口)执行语句,主要是openssl的语句通过黑窗口执行会报错,具体原因还不是很清楚,所以通过调用脚本的方式达到目的。另外还有一个Linux环境默认安装的ffmpeg无法进行视频加密操作,需要安装一系列的依赖,安装方式可以参考下面的方式,我用过其他方式安装无法进行视频截取,生成key相关的资料可以参数这个:使用ffmpeg视频切片并加密
安装ffmpeg
yum -y install epel-release
rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-1.el7.nux.noarch.rpm
yum -y install ffmpeg ffmpeg-devel
ffmpeg -version
使用指令测试切分情况
ffmpeg -i 22.mp4 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls index.m3u8
22.mp4代表要切分视频名称,hls_time 代表每个ts的时长,index.m3u8代表输出的文件路径和名称
代码
本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试 本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试 本代码只是用Linux系统,请不到在windows上测试,代码写好后打包到Linux上测试
相关依赖
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
<!-- IOUtils.toString()方法用到了-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.3</version>
</dependency>
代码实现的效果是从oss上下载视频到本地,做视频切分和加密后重传到oss中,要求oss中的bucket权限为:公开读-公开读-公开读
涉及的方法比较多,由于这是项目的代码不方便直接复制类分享,所以只能分享方法,有些方法可能在不同的类中,可以自己调整用到的类应该都分享出来了。另外lz建议先将所有的方法放到一个类中,待接口调通后放到工具类方法中,再将不同的方法防到例如FileUtil、M3u8Util、OSSUtil等工具类中
测试接口方法
public static final String ENCRYPT_BUCKET_NAME = "YourBucketName";
public static final String IV_NAME = "iv.txt";
public static final String ENC_KEY_INFO = "enc.keyinfo";
public static final String ENC_KEY = "enc.key";
@ResponseBody
@GetMapping("/mp4Tom3u8AndEncrypt")
public ResponseResult mp4Tom3u8AndEncrypt() {
mp4Tom3u8AndEncrypt("YourBacketName", "upload/", "a.mp4");
return ResponseResult.success();
}
oss中的文件 mp4Tom3u8AndEncrypt方法
- endpoint, accessKeyId, accessKeySecret使用自己的不知道怎么获取的可以查看阿里云文档
public static String mp4Tom3u8AndEncrypt(String bucketName, String path, String name) {
String objectName = path + name;
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
boolean found = ossClient.doesObjectExist(bucketName, objectName);
if (!found) {
throw new BusinessException(CommonExEnum.OSS_FILE_NOT_EXIST);
}
String storagePath = downloadToLocal(bucketName, path, name);
String storageFilePath = storagePath.substring(0, storagePath.indexOf(name));
log.info(storageFilePath);
String encKey = generateRandom(16);
log.info(encKey);
createFileWriterContent(storageFilePath, ENC_KEY, encKey);
generateIVFile(storageFilePath);
String content = ENC_KEY + "\n" +
storageFilePath + File.separator + ENC_KEY + "\n" +
FileUtil.readFileContent(storageFilePath + File.separator + IV_NAME);
log.info("***keyinfo文件的内容为:{}***", content);
createFileWriterContent(storageFilePath, ENC_KEY_INFO,
content);
dealMp4ToM3u8AndEnc(storageFilePath + File.separator + ENC_KEY_INFO,
storageFilePath + File.separator + name);
int duration = calculateVideoDuration(storageFilePath + File.separator + name);
log.info("***视频时长为:【{}】秒***", duration);
int fragmentNum = (int) Math.ceil((float) duration / 20);
log.info("***ts数量为:{}***", fragmentNum);
waitVideoSegmentFinish(storageFilePath, fragmentNum);
List<String> uploadPathList = getUploadPathList(storageFilePath);
String filePrefix = name.substring(0, name.indexOf("."));
for (String uploadFile : uploadPathList) {
File file = new File(uploadFile);
String uploadFileName = file.getName();
String fileName = "security/" + filePrefix + "/" + uploadFileName;
try {
String s = uploadFileWithInputSteam(ENCRYPT_BUCKET_NAME, fileName,
new FileInputStream(file));
log.info("***文件上传后的绝对路径为:【{}】***", s);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
return "";
}
下面的方法是mp4Tom3u8AndEncrypt中用到的方法
downloadToLocal方法 注意
- 替换endpoint, accessKeyId, accessKeySecret参数
- /opt/nginx/html/m3u8/ 字符串换成自己服务器上nginx的html目录并且nginx.conf文件需要做配置,默认不支持播放m3u8文件,配置防止可以在Java实现视频加密及播放 找
public static String downloadToLocal(String bucketName, String path, String name) {
String objectName = path + name;
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
String tempPath;
String osName = System.getProperty("os.name");
String fileName = name.substring(0, name.indexOf("."));
if (osName.startsWith("Windows")) {
tempPath = "C:\\Users\\Administrator\\Desktop\\m3u8\\codeEncrypt\\" + fileName;
} else {
tempPath = "/opt/nginx/html/m3u8/" + fileName;
}
File uploadFile = new File(tempPath);
if (!uploadFile.exists()) {
log.info("创建的文件夹目录:{}", uploadFile.getPath());
uploadFile.mkdirs();
}
ossClient.getObject(new GetObjectRequest(bucketName, objectName),
new File(tempPath + File.separator + name));
ossClient.shutdown();
return tempPath + name;
}
generateRandom方法 生成一定长度的字母与数字随机数
public static String generateRandom(int length) {
StringBuilder val = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
if ("char".equalsIgnoreCase(charOrNum)) {
int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
val.append((char) (random.nextInt(26) + temp));
} else if ("num".equalsIgnoreCase(charOrNum)) {
val.append(String.valueOf(random.nextInt(10)));
}
}
return val.toString();
}
createFileWriterContent方法
public static void createFileWriterContent(String filePath, String fileName, String content) {
File dir = new File(filePath);
if (!dir.exists()) {
dir.mkdirs();
}
File checkFile = new File(filePath + File.separator + fileName);
FileWriter writer = null;
try {
if (!checkFile.exists()) {
checkFile.createNewFile();
}
writer = new FileWriter(checkFile);
writer.append(content);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != writer) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
generateIVFile方法 注意
- 这个方法会运行服务器中/data/shell的脚本,目录下有两个脚本,在这个方法下直接复制出来
public static void generateIVFile(String path) {
List<String> command = new ArrayList<>();
command.add("sh");
command.add("/data/shell/generate-iv.sh");
command.add(path + File.separator);
System.out.println("command命令:{}" + command.toString());
ProcessBuilder builder = new ProcessBuilder(command);
try {
Process start = builder.start();
BufferedReader br2 = new BufferedReader(new InputStreamReader(start.getErrorStream()));
StringBuilder buf = new StringBuilder();
String line;
while ((line = br2.readLine()) != null) buf.append(line);
System.out.println("执行生成iv文件脚本返回输出结果:{}" + buf.toString());
} catch (IOException e) {
e.printStackTrace();
}
log.info("***生成iv文件完成***");
}
generate-ts-video.sh脚本,防至/data/shell/目录下 $1、$2、$3代表脚本参数,调用脚本时传入
ffmpeg -i $1 -profile:v baseline -level 3.0 -start_number 0 -hls_time 20 -hls_list_size 0 -f hls -hls_key_info_file $2 $3
generate-iv.sh脚本,防至/data/shell/目录下
openssl rand -hex 16 > $1iv.txt
如图
dealMp4ToM3u8AndEnc方法
/**
* 将mp4文件切分成ts文件,并根据keyinfo文件加密
* generate-ts-video.sh脚本中ffmpeg切分指令:
* ffmpeg -i a.mp4 -profile:v baseline -level 3.0 -start_number 0
* -hls_time 20 -hls_list_size 0 -f hls -hls_key_info_file enc.keyinfo index.m3u8
*
* @param sourcePath 需要加密的视频路径
* @param keyInfoPath keyinfo文件的路径
* @throws IOException
*/
public static void dealMp4ToM3u8AndEnc(String keyInfoPath,
String sourcePath) {
List<String> command = new ArrayList<>();
command.add("sh");
command.add("/data/shell/generate-ts-video.sh");
//需要切分的视频路径
command.add(sourcePath);
//keyinfo文件位置
command.add(keyInfoPath);
//自定义切分名称,同时包含m3u8文件的输出路径
command.add(sourcePath.replace("mp4", "m3u8"));
long start = System.currentTimeMillis();
ProcessBuilder builder = new ProcessBuilder(command);
try {
builder.start();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info(Arrays.toString(builder.command().toArray()));
log.info("总共耗时=" + (end - start) + "毫秒");
}
calculateVideoDuration方法
/**
* 计算视频的时长
*
* @param storageFilePath 视频路径
* @return 视频时长,单位为秒
*/
public static int calculateVideoDuration(String storageFilePath) {
//生成获取视频时长的指令
String dos = "ffprobe " + storageFilePath;
//开始时间
long time = System.currentTimeMillis();
String dosText = execDos(dos);
System.out.println("耗时:" + (System.currentTimeMillis() - time) + "毫秒");
System.out.println("dos文本:" + dosText);
int startIndex = dosText.indexOf("Duration: ") + "Duration: ".length();
System.out.println("startIndex:" + startIndex);
int endIndex = dosText.indexOf(", start", startIndex);
System.out.println("endIndex:" + endIndex);
String durationText = dosText.substring(startIndex, endIndex).trim();
System.out.println("视频时长文本为:{}" + durationText);
String[] arr = durationText.split(":");
//计算视频总时长(单位:秒),每20秒切一次
int duration = 0;
//计算小时单位的数量
if (!"00".equals(arr[0])) {
duration = (Integer.parseInt(arr[0]) * 3600);
}
if (!"00".equals(arr[1])) {
duration += (Integer.parseInt(arr[1]) * 60);
}
if (!"00".equals(arr[2]) && !"00.00".equals(arr[2])) {
String secondStr = arr[2];
String[] split = secondStr.split("\\.");
duration += Integer.parseInt(split[0]);
if (!"00".equals(split[1])) {
//带有毫秒的单位不为0,则默认将视频长度加1s
duration++;
}
}
log.info("视频时长为: 【{}】 秒", duration);
return duration;
}
exec方法
private static String execDos(String dos) {
try {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(dos);
process.waitFor();
InputStream in = process.getErrorStream();
return IOUtils.toString(in, StandardCharsets.UTF_8);
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
return null;
}
waitVideoSegmentFinish方法
public static void waitVideoSegmentFinish(String storageFilePath, int fragmentNum) {
long start = System.currentTimeMillis();
int entryTime = 0;
do {
if (entryTime > fragmentNum * 4) {
break;
}
try {
Thread.sleep(3000);
entryTime++;
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (getAllFileNameCountTsNumber(storageFilePath) < fragmentNum);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info("***本次切片耗时:【{}】秒***", (end - start) / 1000);
}
getAllFileNameCountTsNumber方法
private static int getAllFileNameCountTsNumber(String directoryPath) {
List<String> list = new ArrayList<>();
File baseFile = new File(directoryPath);
if (baseFile.isFile() || !baseFile.exists()) {
return 0;
}
File[] files = baseFile.listFiles();
for (File file : files) {
if (!file.isDirectory()) {
list.add(file.getName());
}
}
int count = 0;
for (String s : list) {
int start = s.lastIndexOf(".") + 1;
String suffix = s.substring(start);
if ("ts".equals(suffix)) {
count++;
}
}
return count;
}
getUploadPathList方法
public static List<String> getUploadPathList(String basePath) {
List<String> list = new ArrayList<>();
File baseFile = new File(basePath);
if (baseFile.isFile() || !baseFile.exists()) {
return null;
}
File[] files = baseFile.listFiles();
for (File file : files) {
if (!file.isDirectory()) {
String name = file.getName();
int start = name.lastIndexOf(".") + 1;
String suffix = name.substring(start);
if ("ts".equals(suffix) || "m3u8".equals(suffix) || "key".equals(suffix)) {
list.add(file.getAbsolutePath());
}
}
}
return list;
}
uploadFileWithInputSteam方法
public static String uploadFileWithInputSteam(String bucketName, String oranFileName, InputStream inputStream) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
ossClient.putObject(bucketName, objectName, inputStream);
} catch (Exception ex) {
ex.printStackTrace();
}
ossClient.shutdown();
return getRealName(bucketName, objectName);
}
代码编写与整理都花了很长时间、代码中有很多注释,如果漏有什么地方没有分享到可以给我留言我一般工作日一天之内就会回复,如果对你有帮助希望点个赞哦
|