mp4视频分片生成m3u8流文件并加密
目录
场景描述
相信大家都有这样的经历,一个视频太大,放到服务器上面,播放的时候,受服务器宽带和自己网络的影响访问会很慢。
经常看视频的小伙伴肯定看到过下面的场景,网页上视频播放的时候,会有一个m3u8的请求,然后再去请求key,再一个ts一个ts的下载。

这样有什么好处呢?就是当你视频五六百兆的大小,你可以通过工具将视频切片成一个个5兆大小的ts。然后生成m3u8文件,m3u8文件中包含每一个ts的名字和加密的key。关于m3u8这里不做介绍,不知道的可以百度一下。这样播放的时候,会一个一个的去获取ts文件,因为ts文件的大小5兆左右,一般来说,普通服务器上就能很快的访问到。
还有一个场景,比如说公司要求在视频播放快的基础上,还要保证视频不能被别人下载,也可以采用这个方式,将mp4切片然后加密。然后对key的获取做限制,这样别人也就无法下载你的视频了。
下面进入正题。
将一个mp4视频文件切割为多个ts片段,并在切割过程中对每一个片段使用 AES-128 加密,最后生成一个m3u8的视频索引文件;
需要的环境,JDK,和 ffmpeg。ffmpeg 的安装,可以百度一下,这里就不说了。
加密准备
生成enc.key
openssl rand 16 > enc.key ( 生成一个enc.key文件 )
生成 iv
openssl rand -hex 16 ( 生成一段字符串,记下来)

新建一个文件 enc.keyinfo 内容格式如下:
Key URI # enc.key的路径,使用http形式
Path to key file # enc.key文件
IV # 上面生成的iv
举个例子:enc.keyinfo内容如下:
https://xxx/maps/enc.key
/Users/bukesu/test/enc.key
28c81707c38a3221a7e0b932868e95d7
其中 https://xxx/maps/enc.key 你需要提供对外的接口,可以通过接口访问到enc.key文件的内容,你可以放到自己的web项目中,也可以放在nginx下面,通过natapp本地穿透提供给外部访问,我这里是用的是natappp配置的内网穿透。通过访问https://xxx/maps/enc.key可以下载enc.key
接下来咱们进行视频的分片
视频分片
这里先用命令进行视频分片加密
ffmpeg -y \
-i "/Users/bukesu/test/test.mp4" \
-hls_time "10" \
-hls_key_info_file "/Users/bukesu/test/enc.keyinfo" \
-hls_playlist_type "vod" \
-hls_list_size "0" \
-hls_segment_filename "/Users/bukesu/test/ts/file%d.ts" \
"/Users/bukesu/test/ts/playlist.m3u8"
hls_time:指定生成 ts 视频切片的时间长度s
hls_key_info_file:enc.keyinfo文件的位置
hls_list_size: 索引播放列表的最大列数 默认5,0 为不限制
hls_segment_filename:生成ts和m3u8文件的路径及文件名
到这里,视频分片加密就完成了,如何验证呢?
首先、配置nginx映射,
location /maps/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
add_header Access-Control-Allow-Headers "Origin, Authorization, Accept";
add_header Access-Control-Allow-Credentials true;alias "/Users/bukesu/test/";
autoindex on; #开启nginx目录浏览功能autoindex_exact_size off; #文件大小从KB开始显示
autoindex_localtime on; #显示文件修改时间为服务器本地时间
}
启动nginx,我这里nginx使用的是6699端口,访问 http://localhost:6699/maps/ts/playlist.m3u8 可以下载playlist.m3u8文件,
将enc.key 也放到test目录下,所以访问 http://localhost:6699/maps/enc.key 也能进行下载
接下来将natapp 映射到nginx端口上,natapp的使用方法我这里就不详细说明了,配置如下

启动natapp,上面的两个地址
http://localhost:6699/maps/ts/playlist.m3u8
http://localhost:6699/maps/enc.key
就可以替换成域名进行访问了
https://xxx.com/maps/ts/playlist.m3u8
https://xxx.com/maps/enc.key
接下来开始验证,在浏览器输入 https://www.m3u8play.com/ 网址,输入https://xxx.com/maps/ts/playlist.m3u8,如下图:

到这里 就出现了和文章最开始一样的效果了。
那么,java代码如何实现呢?
Java代码实现
新建 FFMpegUtils 工具类
@Slf4j
public class FFMpegUtils {
public static String executeCommand(List<String> commonds,String FFMPEG_PATH) throws InterruptedException, IOException {
log.info("开始视频分片");
if (CollectionUtils.isEmpty(commonds)) {
log.error("--- 指令执行失败,因为要执行的FFmpeg指令为空! ---");
return null;
}
LinkedList<String> ffmpegCmds = new LinkedList<>(commonds);
ffmpegCmds.addFirst(FFMPEG_PATH); // 设置ffmpeg程序所在路径
log.info("--- 待执行的FFmpeg指令为:---" + ffmpegCmds);
Runtime runtime = Runtime.getRuntime();
Process ffmpeg = null;
try {
// 执行ffmpeg指令
ProcessBuilder builder = new ProcessBuilder();
builder.command(ffmpegCmds);
ffmpeg = builder.start();
log.info("--- 开始执行FFmpeg指令:--- 执行线程名:" + builder.toString());
// 取出输出流和错误流的信息
// 注意:必须要取出ffmpeg在执行命令过程中产生的输出信息,如果不取的话当输出流信息填满jvm存储输出留信息的缓冲区时,线程就回阻塞住
PrintStream errorStream = new PrintStream(ffmpeg.getErrorStream());
PrintStream inputStream = new PrintStream(ffmpeg.getInputStream());
errorStream.start();
inputStream.start();
// 等待ffmpeg命令执行完
ffmpeg.waitFor();
// 获取执行结果字符串
String result = errorStream.stringBuffer.append(inputStream.stringBuffer).toString();
// boolean blank = StringUtils.isBlank(result);
// 输出执行的命令信息
String cmdStr = Arrays.toString(ffmpegCmds.toArray()).replace(",", "");
System.err.println(result);
String resultStr = StringUtils.isBlank(result) ? "【异常】" : "正常";
log.info("--- 已执行的FFmepg命令: ---" + cmdStr + " 已执行完毕,执行结果: " + resultStr);
return result;
} finally {
if (null != ffmpeg) {
ProcessKiller ffmpegKiller = new ProcessKiller(ffmpeg);
// JVM退出时,先通过钩子关闭FFmepg进程
runtime.addShutdownHook(ffmpegKiller);
}
}
}
/**
* 用于取出ffmpeg线程执行过程中产生的各种输出和错误流的信息
*/
static class PrintStream extends Thread {
InputStream inputStream = null;
BufferedReader bufferedReader = null;
StringBuffer stringBuffer = new StringBuffer();
public PrintStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
try {
if (null == inputStream) {
log.error("--- 读取输出流出错!因为当前输出流为空!---");
}
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
log.info(line);
stringBuffer.append(line);
}
} catch (Exception e) {
log.error("--- 读取输入流出错了!--- 错误信息:" + e.getMessage());
} finally {
try {
if (null != bufferedReader) {
bufferedReader.close();
}
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
log.error("--- 调用PrintStream读取输出流后,关闭流时出错!---");
}
}
}
}
/**
* 在程序退出前结束已有的FFmpeg进程
*/
private static class ProcessKiller extends Thread {
private Process process;
public ProcessKiller(Process process) {
this.process = process;
}
@Override
public void run() {
this.process.destroy();
log.info("--- 已销毁FFmpeg进程 --- 进程名: " + process.toString());
}
}
}
新建 VideoDemo类
@Slf4j
public class VideoDemo {
public static void main(String[] args) throws IOException, InterruptedException {
videoToTs("/Users/bukesu/test/test.mp4" );
}
public static String videoToTs(String objectName) throws IOException, InterruptedException {
File file = new File(objectName);
// 获取文件路径
String parentPath = file.getParent();
log.info("准备视频分片");
String targetPath = parentPath + "/ts";
FileUtil.mkdir(targetPath);
List<String> commands = new ArrayList<>();
commands.add("-i");
commands.add(file.getAbsolutePath());
commands.add("-profile:v");
commands.add("baseline");
commands.add("-level");
commands.add("3.0");
commands.add("-start_number");
commands.add("0");
commands.add("-hls_time"); //指定生成 ts 视频切片的时间长度s
commands.add("20");
//加密 需要借助openssl生成enc.keyinfo文件
commands.add("-hls_key_info_file");
commands.add("/Users/bukesu/test/enc.keyinfo");
commands.add("-hls_list_size"); //索引播放列表的最大列数 默认5,0 为不限制
commands.add("0");
commands.add("-hls_segment_filename"); //输出 ts m3u8 文件路径
commands.add(targetPath + "/file%d.ts");
commands.add(targetPath + "/playlist.m3u8");
FFMpegUtils.executeCommand(commands, "/usr/local/bin/ffmpeg");
log.info("视频分片结束");
return targetPath;
}
}
运行main方法就可以进行分片加密了。
那之前说的enc.key,放在nginx下没做任何校验,肯定是不行的,所以这里就可以将enc.key放在项目resources目录下,如图:

然后写一个接口获取 enc.key
String aesKey = "eb367ce6e5bhjg686t6F65r688767";
String key_vi = "0000000000000000";
@ApiOperation("获取enc.key")
@GetMapping("/enc/enc.key")
public void getEncKey(HttpServletRequest request, HttpServletResponse response) throws IOException {
String header = request.getHeader("vhlsk");
log.info("header:{}", header);
if (StringUtils.isBlank(header)) {
log.warn("没有请求头");
return;
}
header = AESUtil.decode(aesKey, header,key_vi);
// 当前时时间戳
Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
Long offset = 300000L;//5分钟
// 传过来的时间戳
String substring = header.substring(5);
long aLong = Long.parseLong(substring);
// 接收到的时间戳和当前时间戳 在 5分钟范围内 有效
if (aLong > (milliSecond - offset) && aLong < (milliSecond + offset)) {
Resource resource = new DefaultResourceLoader().getResource("classpath:enc/enc.key");
response.setContentType("application/force-download");
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode("enc.key", StandardCharsets.UTF_8));
@Cleanup InputStream inputStream = resource.getInputStream();
@Cleanup ServletOutputStream servletOutputStream = response.getOutputStream();
IOUtils.copy(inputStream, servletOutputStream);
response.flushBuffer();
} else {
log.warn("传入的header无效");
}
}
这里前端播放m3u8视频的时候,需要修改播放器请求头,获取 enc.key 时,加入了自定义请求头vhlsk,值是当前时间戳,时间戳前端通过AES进行加密,后台通过AES进行解密,然后判断判断在5分钟内就返回有效的enc.key。
这里的验证比较简单,如果想更加复杂,可以自己和前端对校验算法进行复杂化。
这样就可以防止别人下载你的视频了。
以上是个人理解,如有不对的地方,欢迎指正。
Tip:若有疑难问题需帮助,可注明来意,加V:w449044976
智能推荐
python实现将m3u8格式转换为mp4视频格式
开发动机:最近用手机QQ浏览器下载了一些视频,视频越来越多,占用了手机内存,于是想把下载的视频传到电脑上保存,可后来发现这些视频都是m3u8格式的,且这个格式的视频都切成了碎片,存在电脑里不方便查看,于是想把它转换为其他可以直接打开播放的完整视频,到网上找了一些工具,都不怎么好用,后来发现一个手机端的&ldquo...
使用ffmpeg从m3u8地址下载mp4视频文件-linux脚本(bash)
1.使用说明 如图: 2.脚本如下: 注意: 将第一行#!/usr/bin/env bash修改为你的bash的绝对路径. 在线m3u8播放器 参考:m3u8_download...
web安全简易规范123
web安全,大公司往往有专门的安全开发流程去保证,有专门的安全团队去维护,而对于中小网络公司,本身体量小,开发同时兼带运维工作,时间精力有限,但是,同样需要做一些力所能及的必要的事情。有时候,安全威胁并不是因为你的防盗窗被人撬开了,而是你晚上睡觉的时候忘了关门,而关上门对开发来说也许只是举手之劳。 1、不要用root,确定使用的中间件和框架是否默认打开了后门 我们总会在线上使用部署一些中间件、开源...
html5拖放--15行js代码实现两个div内容互换
本文首发于我的个人博客:http://cherryblog.site/ ,欢迎大家前去参观 本文项目地址,sortable插件地址:https://github.com/sunshine940326/sortable demo地址:https://github.com/sunshine940326/drag 在写我们后台的管理程序中需要有一个拖放的功能,然后我们有一个这样的功能,实现11个固定且大...
猜你喜欢
git切换分支报错,不管什么标题名字,都报非法字符,所以就不起名字了。
切换分支的时候,报了标题这么个错误,error: ”xxx did not match any file(s) known to git. 看见不能切换分支,我首先 git status 查看了一下当前状态,如下图 然后,就会发现,其实我的这个错误非常明显,就是在我的 beat 分支下有文件修改,所以切换不了。ok,解决方法: 1. 如果修改的这些文件没什么用,完全可以删除。(我这儿的...
Oracle分析函数之LEAD和LAG实际应用
Oracle分析函数之LEAD和LAG实际应用 在前几天的工作中按照客户的需求,需要对客户信息进行数据分析,即某人存在多个状态的账号,将客户信息账号状态分析出结果,和客户确认汇报,根据保留规则,保留唯一账号,以保证程序可用性。起初,根据聚合函数进行查询分析,需要写一大串的SQL,即不美观又复杂,很容易产生错误。后续想到Oracle分析函数中的lead和lag,SQL简洁了很多且容易产生报告数据。 ...
小知识积累(不断更新中)
判断变量的类型(数组,对象) tyopof:不推荐,因为无法区别数组与对象,数组是对象的子对象 instanceof:可以使用 还可以用来判断是否属于函数 Object.prototype.toString.call():最兼容,推荐使用 定时器的执行顺序或机制 js是单线程的,浏览器遇到setTimeout或者setInterval会把定时器推入浏览器的待执行事件队列里面但是不执行,先执行完当前...
ROS自学实践(6):ROS进行激光SLAM建图——gmapping
本节主要记录运行ROS自带的SLAM建模包gmapping方法,为后续理解这些代码,建立自己的SLAM算法打下基础。 基于粒子滤波算法 二维栅格地图 需要里程计信息 1.通过命令行安装gmapping包 2.配置gmapping节点 3.运行gazebo模型及gmapping节点 4.打开rviz 添加laserscan、map、robotmodel模型 5.移动小车,建立模型 6.保存当前地图 ...
face-api.js中加入MTCNN:进一步支持使用JS实时进行人脸跟踪和识别
如果你现在正在阅读这篇文章,那么你可能已经阅读了我的介绍文章(JS使用者福音:在浏览器中运行人脸识别)或者之前使用过face-api.js。如果你还没有听说过face-api.js,我建议你先阅读介绍文章再回来阅读本文。 和往常一样,本文中为你准备了一个代码示例。我们将解析一个小的应用程序,这个程序将在浏览器中访问摄像头图像执行实时人脸检测和人脸识别,让我们开始吧! 使用face-api.js进行...
