音视频-加密技术方案
问题
针对音视频-加密技术需求,基于现有技术成果基础上,设计音视频被盗方案
首先,想要杜绝音视频被盗,是不可能的。只能提高技术门槛,让有心者盗取相对麻烦,并且我们可以轻松调节密钥,慢慢的让较低损失,逐步集成成熟的防盗方案
为什么选择开源技术
成本低廉,技术成熟,对专业有一定门槛,只需要技术自定义加密手段(这是核心,一切核心在于加密)
注意:我们不该轻易造轮子,利用现有技术,加上核心诉求,成本合理的情况,迅速找到技术解决方案
初步方案设计
技术方案梳理-手稿
技术方案梳理-架构图
流程补充
调研实例
播放器与资源与服务端的交互
m3u8加密是怎么回事
我们看下加密的m3u8文件的格式
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-KEY:METHOD=AES-128,URI="http://xxxxxx:5555//test/1102/test/segments.key"
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:19
#EXTINF:13.966667,
http://xxxxxx:5555/test/1102/test/segments0.ts
#EXTINF:10.000000,
http://xxxxxx:5555/test/1102/test/segments1.ts
#EXTINF:10.000000,
http://xxxxxx:5555/test/1102/test/segments2.ts
#EXTINF:10.000000,
http://xxxxxx.cn:5555/test/1102/test/segments3.ts
#EXTINF:10.000000,
http://xxxxxxn.cn:5555/test/1102/test/segments4.ts
#EXTINF:7.033333,
http://xxxxxx:5555/test/1102/test/segments5.ts
#EXTINF:10.000000,
看到了多了个字段EXT-X-KEY,这个也是m3u8给规定好的加密字段,如果包含这个字段播放器就会先去请求这个key,然后拿这个这个key去访问加密的TS视频就可以播放了(我们可以在这里做文章,服务端在上传文件使用FFmpeg 分片的时候,自定义加密逻辑,客户端每次播放完一个分片后,会通过自定义key解密,才可以读取正确的next ts,才可以继续播放,这是加密的核心)。
在线演示
-
输入加密视频地址:https://test-streams.mux.dev/dai-discontinuity-deltatre/manifest.m3u8
-
打开控制台查看,三端交互
客户端实现播放加密缓存的思路
- 我们首先按照特定的格式去解析这m3u8文件。
- 按照解析出来的ts文件按照我们知道的规则组拼起来,下载这些ts文件,存放在手机的sd卡,这些下载下来的TS视频文件是播放不了的,需要把正确的key下载下来,加上定期的key解密算法,才可以读取这些TS。
- 我们需要在本地搭建一个本地http服务。本地服务器我们通过过滤特定的接口名字,来实现根据不同ts名字返回不同的视频文件(这里最好生成和原始的ts文件的名字一样)
- 我们如何知道播放器播完一段视频呢,因为他是一段一段播放的,所以这里就需要我们在本地生成一份本地指向我们本地服务器的m3u8文件,直接播放本地的时候直接返给播放器,这里多了我们需要多个加密的key。我们本地增加一个过滤接口的方法,判断如果是访问加密的key就先把key返给播放器,然后播放器会紧接着去请求拿去ts视频这样我们就可以把视频返给他,它内部就会去解密进行播放了。
技术栈
NODE.JS + FFmpeg + HLS + WEB + REACT NATIVE
客户端,沙箱存储逻辑
解析
var fs=RNFetchBlob.fs
var path=RNFetchBlob.fs.dirs.DocumentDir
//建音视频主文件
var videocache=path+'/videocache'
let isDir=await fs.isDir(videocache)
if(!isDir){
await fs.mkdir(videocache)
.then(() => {console.log('创建 videocache 文件成功')})
.catch((err) => {console.log('创建 videocache 文件失败')})
}
var url='http://video.samuredwonder.com/6f94765277492f9b8ab181ce6a22b387%3D%2FlrOn4B7V9FewWz96xCggAUHObdPc%2F0?pm3u8/0/expires/90384&e=1524169405&token=vcZj_ZMWMJ8gU759Lfsd8_A2jgriPXGS6tBJX1Ss:u5YUx1lI_8VzrXrgTOGOvkurKUs=';
let dirs = videocache+'/test.m3u8'
var dirsRes=await this.downloadVideo(url,dirs)
console.log(dirsRes)
//得到m3u8内容
var manifest = await dirsRes.text()
console.log(manifest)
//解析主体m3u8
var parser = new m3u8Parser.Parser();
parser.push(manifest);
parser.end();
var parsedManifest = parser.manifest;
console.log(parsedManifest)
//得到媒体源列表
var segments=parsedManifest.segments
console.log(segments)
if(segments){
this.walk(segments,videocache)
}
下载切片列表,加密ts文件,创建对应音视频目录
async walk(segments,videocache){
this.asyncForEach(segments, async x => {
var qianzui='http://video.samuredwonder.com'
let dirs = videocache+'/'+x.uri
let url = qianzui+x.uri
var res = this.downloadVideo(url,dirs)
console.log(res)
})
}
downloadVideo(fromUrl, toFile){
const activeDownloads = {};
activeDownloads[toFile] = new Promise((resolve, reject) => {
RNFetchBlob
.config({path: toFile})
.fetch('GET', fromUrl)
.then(res => {
if (Math.floor(res.respInfo.status / 100) !== 2) {
throw new Error('Failed to successfully download video');
}
//resolve(toFile);
resolve(res);
})
.catch(err => {
return deleteFile(toFile)
.then(() => reject(err));
})
.finally(() => {
// cleanup
delete activeDownloads[toFile];
});
});
return activeDownloads[toFile];
}
播放沙箱加密音视频
参考
https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
https://zhuanlan.zhihu.com/p/435343511
https://www.cnblogs.com/TheViper/p/4363383.html
https://github.com/videojs/videojs-contrib-hls/blob/master/README.md