打造m3u8視訊(流視訊)下載解密合並器(kotlin)
本文是對我原創工具m3u8視訊下載合併器關鍵程式碼解析及軟體實現的思路的講解,想要工具的請跳轉連結
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流,按照順序依次把流追加到末尾即可
參考
m3u8 檔案格式詳解
關於m3u8格式的視訊檔案ts轉mp4下載和key加密問題
aes 256 32位key和32位iv
加密ts解