记一次HLS视频加解密方案的过程(1)

前言&背景

最近脑壳很疼,事情是这样的。
很久之前,公司的一个app产品要播放视频教学文件,我跟一个哥们一合计,本来想的是弄一个VideoView,封装一个MediaController,简单实用。
后来等原型出来后,我们技术准备也完成了,领导突然说,视频是flv的,当时就傻眼了,换播放器,编译ijk,编译ffmpeg,各种折腾,算是折腾好了,项目也上线了。
再后来,最近,领导说要视频加密,又傻眼了,没弄过啊,我一移动端不会啊,好在当初折腾过NDK,也折腾过ffmpeg,就开始了视频加密。


方案

现在公司移动端就剩我一个人了,ios也跑了。
后台的兄弟天天忙别的,PC端的视频加密,是加密的flv文件,买的别的公司的技术服务,我大致看了下,应该是自己写了个编码器,加密视频进行编码,然后播放的时候用的ffplay,自己套一个自己写的解码器,我感觉我技术还是达不到写编码器解码器这个水平的,就转战研究前人走过的路。
我折腾出了两种方案,大致是:

  • 对mp4静态文件加密。
  • 对mp4视频转为hls视频流。

mp4静态文件加密

这个方案很简单,原理就是先把mp4文件通过流读取,然后再保存,过程插入加密的字符就ok了。

  • 优点:操作起来简单,不用耗工时。
  • 缺点:一是加密的太简单了,二来没办法做到即时播放,一定要下载到手机上才可以播放,很鸡肋。

加密代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.QingHeYang;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class ChangeMp4 {

private static String sourcePath = "E:/source.mp4";
private static String targetPath = "E:/target.mp4";

public static void main(String[] args) {
encodeMp4();
}

public static void encodeMp4(){
File sourceFile = new File(sourcePath);
File targetFile = new File(targetPath);
FileInputStream input = null;
BufferedInputStream inBuff = null;
FileOutputStream output = null;
BufferedOutputStream outBuff = null;

try {
input = new FileInputStream(sourceFile);
inBuff = new BufferedInputStream(input);
output = new FileOutputStream(targetFile);
outBuff = new BufferedOutputStream(output);

outBuff.write(encrypt("value", "key"));
byte[] b = new byte[1024 * 5];
int len;
while ((len = inBuff.read(b)) != -1) {
outBuff.write(b, 0, len);
}
outBuff.flush();
inBuff.close();
outBuff.close();
output.close();
input.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outBuff.flush();
inBuff.close();
outBuff.close();
input.close();
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static byte[] encrypt(String content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
}

加密效果还行吧,只能说能糊弄一下不懂的人,效果如图。
未加密的二进制文件:
未加密
加密的二进制文件对比:
加密
在头部加了AES-128加密的字符,其实不一定在头部加,可以在任意地方加,就强度来说,还算可以。
但是有一个问题,因为我用的ijkPlayer,这个播放器是用ffmpeg编译解码器,然后用ffplay播放画面,输出到android的surfaceView上面的,所以,在Java的api层面,是找不到对这个文件进行流的操作的。
所以,这就涉及到另一个问题,我该怎么播放了,众所周知的是,无论是VideoView还是市面上的ijk也好,vitamio,更原始一点的surfaceView,都有setVideoPath这个api,都没有ImageView的类似setStream这种对文件流的操作。
所以,这就意味着阻断了在Java的api的对视频文件的流操作(我是真的追源码了,最后追到jni层,没法继续下去了),也就意味着没法编解码边播放了,只能下载下来,然后用File的方法,进行decode,这样不是很友好的,因为,一但下载下来,再decode,这样一是浪费了Android手机宝贵的内存控件,二来,解密完后,视频源文件不还是成了未加密的了吗!这不是白玩了吗。
其实也不是不能边解密边播,但是目前来,我的水准还是达不到的,但是大致思路就是,用ijkPlayer编译的过程自定义视频解码器,把自己的视频解码器替换调mp4的解码器,这样,能做到边解密边播(很遗憾,水平不够)。


HLS视频流

放弃掉mp4解密的大坑后,我转战HLS流的形式。
这个形式,好处很多,市面上最流行的,文件加密性好,有链保护,获取不到源文件,获取到了,外行也不会操作,balabala….
说一下优缺点:

  • 优点:技术成熟,会编译,明白原理基本就ok了,加密性好。
  • 缺点:不好说,这个缺点多半是我自身缺点,水平有限。

做HLS视频流要准备一些东西,我列一下:

  • linux:要用到这个编译ffmpeg,看一下ffmpeg官网,windows也可以,不过会很麻烦。
  • ffmpeg源码:下载下来要编译的。
  • ijkPlayer源码:播放加密视频用。点击这里去ijkPlayer
  • mp4源文件:原材料要准备好。
  • tomcat/nginx/apache:服务器,必需品,hls中key文件,以及视频文件都需要用到。

HLS文件构成

先说一下hsl加密视频流的组成内容:

  • .m3u8文件:视频索引文件。
  • .ts文件:视频片段。
  • .key文件:加密秘钥(AES-128,同mp4加密用的一样的手段)。
  • .keyInfo:加密时用的文件,写入.m3u8里面的过程文件。

HLS是基于MP4生成的,将mp4文件切片,在对每一个切片文件加密,然后将这些切片文件的序列进行集合,生成索引文件.m3u8,一个完整的HLS视频流如下图所示:
未加密:
未加密

加密:
加密
未加密的.ts文件可以用播放器直接打开,且在切片的时候不能修改后缀名。
加密的.ts文件不可以直接用播放器打开,可以在生成切片文件时候修改后缀名。
.m3u8文件的内容如下,这个我就只上加密的了,毕竟这个是重点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:35
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-KEY:METHOD=AES-128,URI="http://ubuntu.shouwangzhe.space/video/source2lock/source2.key",IV=0x67e6af3d7b4117a01831d6b3a8741df1
#EXTINF:34.661933,
source2_0_ts
#EXTINF:26.736822,
source2_1_ts
#EXTINF:29.197778,
source2_2_ts
#EXTINF:31.658733,
source2_3_ts
#EXTINF:33.827711,
source2_4_ts
#EXTINF:32.534667,
source2_5_ts
#EXTINF:22.106889,
source2_6_ts
#EXTINF:5.797844,
source2_7_ts
#EXT-X-ENDLIST

编译ffmpeg

因为这块遇到了些问题,但是最终解决了,就简单说一下步骤,ffmpeg编译网络上有很多教程的。
1.先安装gcc+yasm,这个比较重要,必备。

1
2
3
$ sudo apt update
$ sudo apt-get install gcc
$ sudo apt-get install yasm

2.下载ffmpeg,地址点击这里

1
2
3
$ mkdir ffmpeg
$ cd ffmpeg
$ git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg

3.编译ffmpeg。

1
2
$ cd ffmpeg
$ ./configure --enable-shared --disable-static --disable-doc

然后会执行很久,完成之后make。

1
2
$ make
$ make clean

之后就完成了,大致内容如下
ffmpeg
然后就是对视频的操作了。

加密视频

1.准备好一个source.mp4文件,用于加密,放置在一个文件夹下面,准备好两个文件夹,一个放置加密的,一个放置未加密的,大致如下:
文件夹
2.生成要加密的key,前提要有openssl。

1
2
3
$ cd source2lock
$ openssl rand 16 > source2.key
$ cat source2.key

保存下生成的这个偏移量。
3.生成偏移量 IV。

1
$ openssl rand -hex 16 >source2.iv.txt

4.生成hls_key_info_file。

1
2
$ touch source2.keyinfo
$ vi source2.keyinfo

5.用vi写入如下内容。

1
2
3
4
5
6
#你要存秘钥的地址,最好是个网络地址
http://ubuntu.shouwangzhe.space:1994/video/source2lock/source2.key
#刚才你生成的key文件
source2.key
#刚才你保存的偏移量
67e6af3d7b4117a01831d6b3a8741df1

6.视频切片+加密,记得将这些命令写到一行上去,我这么些是因为容易看一些。

1
2
3
4
5
6
7
8
9
ffmpeg  
-i ../source2.mp4
-vcodec copy
-acodec copy
-vbsf h264_mp4toannexb
-hls_time 30
-hls_key_info_file source2.keyinfo
-hls_playlist_type vod
-hls_segment_filename "source2_%d_ts" source2_list.m3u8

出现以下结果就成功了,注意小红框的内容,crypto,后续用的到
文件夹
7.视频未加密,也顺带给上一个未加密的命令吧。

1
2
3
4
5
6
7
8
ffmpeg  
-i ../source2.mp4
-codec copy
-vbsf h264_mp4toannexb
-map 0
-f segment
-segment_list source2_list.m3u8
-segment_time 30 source2_%d.ts

转为在线视频流

有加密视频文件了,就需要一个http服务器来支撑播放。
大致有三种方式,一种比一种简单:

  • nginx:vi /etc/nginx/sites-avaliable/default 就ok,配置端口,静态文件夹
  • tomcat:最常用的一种,将视频文件夹扔到webapps下面即可,然后./startup就ok的。
  • apache:也是经常用到的http服务器,但是我没用过,就不详谈了。

但是无论上述哪一种一定记得,key所在的位置就是刚才加密视频的时候编写hls_key_info_file里面key的位置。

播放加密视频

加密完视频后,可以算是成功一半了,剩下的就是解密视频了。
我记得ijkPlayer支持播放加密的m3u8的问题,于是乎,我直接把视频地址扔进ijkPlayer中,想着应该大功告成了,但是天道好轮回,苍天绕过谁。
ffmpeg
提示是我自己编写的,ijkPlayer是我曾经编译的。
经过研究发现个问题,我这个ijk,好像不支持加密的hls啊!
上一下当时的loagcat:
ffmpeg
可以看到第一,我的key没有问题,第二视频列表已经索引出来了。
但是唯独加载不出来视频片段1。
经过我漫长的寻找解决答案,终于碰到了个明白人,解决了我解密hls播放不出来的问题。
地址:https://segmentfault.com/q/1010000006751909
因为在我ijk编译的过程中,选择的最简略的解码器,外加协议上面没有添加多种协议。
所以,我重新编译了ijkPlayer。
因为过程太过复杂,具体请看github上面的教程。
地址:https://github.com/bilibili/ijkplayer
这里我给一下我服务器上面的已经编译好的包,免去繁琐的编译过程啦。
ijk编译好的包下载地址:点击下载
具体用法请自行百度。
上一下贼简单的activity代码(不然证明不了我是Android程序员啊):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package sohero.com.testhlsvideo;

import android.content.Context;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import sohero.com.testhlsvideo.video.media.IjkVideoView;
import tv.danmaku.ijk.media.player.IMediaPlayer;

public class MainActivity extends AppCompatActivity {

IjkVideoView vv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
vv = (IjkVideoView) findViewById(R.id.ijk_vv);
vv.setVideoPath("http://192.168.31.222:8090/coursePackage/hls/lock/source2_list.m3u8");
vv.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(IMediaPlayer iMediaPlayer) {
vv.start();
}
});
}
}

效果图:
效果图

将来的方向

这套还是有些问题的,例如对key文件保存的不周到,加密的不够好,懂行的人也是能获取全链的,等等等等。
将来的方向我还是想着以操作m3u8文件为主,对视频链路进行保护,着重保护key文件,二次加密等等,现在先这样,等想出来我再补充。


最后

全部搞完历时三天,从不明白HLS,到最后实现出来,总体来说还是挺有成就感的,这大概就是做一个开发工程师的快乐所在吧,如果这篇文章有幸被你看到了,你有什么不懂的地方欢迎留言,我写的毕竟还是太粗糙了,我会尽我所能去帮你,就这样。
以上

请我喝杯咖啡吧~

支付宝
微信