打造m3u8视频(流视频)下载解密合并器(kotlin)
本文是对我原创工具关键代码解析及软件实现的思路的讲解,想要工具的请跳转链接
1.思路说明
思路挺简单,具体步骤如下:
- 下载m3u8文件
- 解析m3u8文件获得ts文件列表
- 根据文件列表批量下载ts文件
- 进行ts的解密操作(如果没有加密则跳过此步骤)
- 将解密后的文件或未加密的ts文件按照m3u8中的列表顺序进行合并,得到mp4文件
可以把kotlin看作为java语言的增强版,java中的知识kotlin也是通用的
本文涉及到知识如下:
- string字符串的处理
- io流,读文件进行读写
- 网络编程
- aes解密(其实我也不是很懂)
2.m3u8格式介绍
可能这个格式大家不是很了解,其实现在大家看的大多数在线视频,都是使用了m3u8文件来实现在线视频播放的。
m3u8 是 unicode 版本的 m3u,用 utf-8 编码。"m3u" 和 "m3u8" 文件都是苹果公司使用的 http live streaming(hls) 协议格式的基础,这种协议格式可以在 iphone 和 macbook 等设备播放。
简单地来说,m3u8就是一个播放列表,里面保存这多个短视频的地址,之后服务器从此文件中按照顺序依次下载ts文件并进行播放。
ts文件也可以看做为mp4文件,可以直接拿qq影音等软件打开,但这只限于未加密的ts文件
可能有些小伙伴会发现, 有些ts文件直接打开软件会提示不支持解析此文件,这其实就是因为ts文件已经被加密了。
我们可以以文本的方式打开m3u8的文件,内容如下:
#extm3u #ext-x-targetduration:10 #extinf:9.009, http://media.example.com/first.ts #extinf:9.009, http://media.example.com/second.ts #extinf:3.003, http://media.example.com/third.ts ...
上面的是未加密的m3u8文件内容,我们来看看加密的m3u8文件:
#extm3u #ext-x-version:3 #ext-x-targetduration:10 #ext-x-media-sequence:0 #ext-x-key:method=aes-128,uri="key.key" #extinf:10.000000, 00000.ts #extinf:10.000000, 00001.ts #extinf:10.000000, 00002.ts #extinf:10.000000 ...
ps:想要了解m3u8格式更多的资料,请查看我底下的参考链接
这里提一下获取m3u8文件的方式,可以通过浏览器f12进入调试模式,之后找到m3u8的网址资源,或者是通过猫抓(chrome插件)
获取链接,猫抓插件安装请自行百度
3.解析m3u8文件获取信息
由上面我们大概了解到了m3u8文件里面的内容,我们将m3u8文件下载到本地之后,可以得到两个信息,key文件地址(如果采用了加密的话)和全部的ts文件地址
#extm3u #ext-x-targetduration:10 #extinf:9.009, http://media.example.com/first.ts #extinf:9.009, http://media.example.com/second.ts #extinf:3.003, http://media.example.com/third.ts ...
上面的这个是没有采用加密的,而且,ts文件都是给出了具体的网址,这是极为理想的情况,但是市面上大部分不会采用这样的,一般都是像下面的这种格式:
#extm3u #ext-x-version:3 #ext-x-targetduration:10 #ext-x-media-sequence:0 #ext-x-key:method=aes-128,uri="key.key",iv=0x12345(可能有) #extinf:10.000000, 00000.ts #extinf:10.000000, 00001.ts #extinf:10.000000, 00002.ts #extinf:10.000000 ...
上面的m3u8文件采用了加密,而且ts文件都是只有编号,没有网址,而且key文件也是非常的简短,根本就不是一个地址,这种情况我们就得进行字符串的拼接处理。
一般的网站,会将m3u8文件、key文件(有加密的话)、ts文件都是放在同一路径
比如说现在有个m3u8的地址为www.xxx.com/2020/1/14/m3u8.m3u8
,使用了加密,所以它的key文件为www.xxx.com/2020/1/14/key.key
,ts文件为www.xxx.com/2020/1/14/0000.ts
上面只是个简单的例子,具体的网站还得具体分析,可以使用抓包进行分析。
现在来对上面的m3u8文件进行简单地分析吧:
采用了aes-128进行了加密,key的地址为key.key
,偏移量iv为12345,有些是没有使用偏移量,则可以使用0来代替
我们通过解析m3u8文件,首先是获得key文件和所有ts文件的地址,然后进行下载即可
通用的下载代码(下载m3u8文件、key文件、ts文件):
/** * 下载文件到本地 * @param url 网址 * @param file 文件 */ private fun downloadfile(url: string, file: file) { val conn = url(url).openconnection() conn.setrequestproperty("user-agent", "mozilla/4.0 (compatible; msie 5.0; windows nt; digext)") val bytes = conn.getinputstream().readbytes() file.writebytes(bytes) }
4.ts文件下载优化
ts文件过多,如果只开启一个单线程进行下载,下载太慢了,所以,可以采用多线程进行下载
这里的话,由于之前解析可以获得一个ts文件地址的列表,把此列表分为几份列表,每份列表开启一个子线程来进行下载,这样便可以保证任务的并发性,提高了下载速度。
这里,稍微有点复杂,因为要把列表划分成几份列表,我大概是这样分的:
首先,计算出列表可以平均分为几份,每份列表的数目,之后再将剩下的列表分为一份,但是,使用循环的话不是很好写,所以就先把第一个列表和最后一个列表分好,之后来个循环,将中间的平分完。
/** * 下载ts文件 * @param threadcount 线程数(默认开启5个线程下载,速度较快,100m宽带测试速度有17m/s) */ fun downloadtsfile(threadcount: int = 5) { val countdownlatch = countdownlatch(threadcount) //每份列表的数目 val step = tsurls.size / threadcount //最后列表的数目(剩下的) val yu = tsurls.size % threadcount //第一份列表 thread { val firstlist = tsurls.take(step) downloadtslist(firstlist) countdownlatch.countdown() } //最后一份列表 thread { val lastlist = tsurls.takelast(step + yu) downloadtslist(lastlist) countdownlatch.countdown() } //中间的平分 for (i in 1..threadcount - 2) { val list = tsurls.sublist(i * step, (i + 1) * step + 1) thread { downloadtslist(list) countdownlatch.countdown() } } countdownlatch.await() println("所有ts文件下载完毕") }
上面的使用了countdownlatch类的对象进行线程的控制,只有当所有线程完成之后,此方法才算结束
5.ts文件解密
先上代码,之后再细讲:
//1.获得key和iv的字符串 val keystring = "2e9515db8fe8358bc8fcf6ae601a00be" val ivstring = "d0817f83115d911241fe8ba17673f120" //2.获得key和iv的bytes数组 val keybytes = decodehex(keystring) val ivbytes = decodehex(ivstring) //3.key数组转为secretkeyspec对象,iv数组转为ivparameterspec val algorithm = "aes" val skey = secretkeyspec(keybytes, algorithm) val iv = ivparameterspec(ivbytes) //4. 初始化cipher val transformation = "aes/cbc/pkcs5padding" val cipher = cipher.getinstance(transformation) cipher.init(cipher.decrypt_mode,skey,iv) //5. 解密, val tsfile = file("q:\\m3u8破解\\2273\\440.ts") val result = cipher.dofinal(tsfile.readbytes()) val newfile = file("q:\\m3u8破解\\2273\\440_s.ts") //6.写入文件 bufferedoutputstream(fileoutputstream(newfile)).write(result)
key文件本质是一个16字节文件,我们可以通过winhex等软件查看里面的内容
不过,查看出来之后的内容,我们还得进行转换,因为是字符串,所以得调用decodehex方法,将字符串转为bytes数组
所以,直接使用代码查看更为方便,kotlin中可以直接读取bytes(如果使用java的话,推荐使用common-io的第三方jar包),如:
val keyfile = file("q:\\test\key.key") //获得bytes数组 val bytes = keyfile.readbytes()
ps:对了,如果m3u8文件中没有使用到iv偏移量,直接使用0即可(要保证bytes数组的长度为16),如果使用了iv的话,要使用decodehex方法转为bytes数组
val ivbytes = if (ivstring.isblank()) bytearrayof(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) else decodehex(ivstring)
/** * 将字符串转为16进制并返回字节数组 */ private fun decodehex(input: string): bytearray { val data = input.tochararray() val len = data.size if (len and 0x01 != 0) { try { throw exception("odd number of characters.") } catch (e: exception) { e.printstacktrace() } } val out = bytearray(len shr 1) try { var i = 0 var j = 0 while (j < len) { var f = todigit(data[j], j) shl 4 j++ f = f or todigit(data[j], j) j++ out[i] = (f and 0xff).tobyte() i++ } } catch (e: exception) { e.printstacktrace() } return out } @throws(exception::class) private fun todigit(ch: char, index: int): int { val digit = character.digit(ch, 16) if (digit == -1) { throw exception("illegal hexadecimal character $ch at index $index") } return digit }
有了key文件和iv偏移量的bytes,我们就可以往下走了,下面的代码其实都没有什么好说明的,明眼人估计一看就懂了,这里就不多说了
需要注意的是,因为解密之后,我们还需要把所有已经解密好的ts文件按照顺序合并成一个mp4文件,所以,注意解密后数据的名字。
建议在保存原来编号的基础上,加上写简短的字母,之后,就可以通过contains方法进行判断是否文件名是否符合条件
6.ts文件合并
合并的话,使用io流,按照顺序依次把流追加到末尾即可
参考
上一篇: 湖南霜降吃什么传统食物