zoukankan      html  css  js  c++  java
  • 打造m3u8视频(流视频)下载解密合并器(kotlin)

    本文是对我原创工具m3u8视频下载合并器关键代码解析及软件实现的思路的讲解,想要工具的请跳转链接

    1.思路说明

    思路挺简单,具体步骤如下:

    1. 下载m3u8文件
    2. 解析m3u8文件获得ts文件列表
    3. 根据文件列表批量下载ts文件
    4. 进行ts的解密操作(如果没有加密则跳过此步骤)
    5. 将解密后的文件或未加密的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:\testkey.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解密

  • 相关阅读:
    Git报错:remote: HTTP Basic: Access denied的解决方法
    扩展模块之ConfigParser模块
    pyftplib
    kafka
    Android MarqueeTextView : 轻松实现文本滚动(跑马灯)效果
    nx
    ScorpionX-RX-64
    SDWebImage源码解读
    iOS 性能优化及AFNetworking源码解析
    vue使用g6做流程图
  • 原文地址:https://www.cnblogs.com/stars-one/p/12198572.html
Copyright © 2011-2022 走看看