1. 起因
最近有个朋友给我发了个小电影。
地址是https://xxxxxx.m3u8, 当我使用微信直接打开的时候是可以播放的,但是使用edge打开的时候却直接跳转到了下载连接里,无奈,只能下载下来一个m3u8的文件。
这边先简单解释一下什么是m3u8的视频格式。
根据维基百科的解释,
M3U8 是 Unicode 版本的 M3U,用 UTF-8 编码。"M3U" 和 "M3U8" 文件都是苹果公司使用的 HTTP Live Streaming(HLS) 协议格式的基础,这种协议格式可以在 iPhone 和 Macbook 等设备播放。
我们使用Visual Studio Code或者其他编辑器打开m3u8文件,我们可以比较清楚地看到,其实就是一个utf-8编码的播放列表。
我们使用播放器播放的时候,实际上是在加载这些地址的视频片段。当我们直接访问这些链接的时候也是可以访问的。但是这样看起来依旧不太舒服,我们已经习惯了看一整段MP4的视频,并且还想把它存到本地自己搭建的NAS里,这样就可以长期播放了不是嘛。
2. 经过
为了方便,我先是下载了一些下载站里的m3u8下载器,但是免费的东西总是会有一定的局限的。这些下载工具虽然UI做的精美,但是尝试了几个之后发现根本不能用。其中有一个倒是成功把每个片段的文件下载下来了。然后再进行合并的时候,根本没有合并成功,却把之前下载的文件全删光了。
要不,还是自己实现吧。
由于只是实现一个小功能,并没有什么性能上的要求,因此决定使用python快速解决。
突然,我在pypi上发现了一个神奇的库。
import m3u8_to_mp4m3u8_to_mp4.download('http://videoserver.com/playlist.m3u8',tmpdir='/tmp/m3u8_xx')
根据提示,只需要两句话就能够把视频文件下载下来,并且使用ffmpeg(该包需要在电脑上先配置ffmpeg, 即执行指令"ffmpeg -version"有结果)将其打包成一个Mp4文件。
于是我兴致冲冲地用了一下,但是结果令人心凉。果然,轮子还是得自己造才行。
3. 造轮子
造轮子也不是从种树开始的,有方便的工具尽管可以使用,这边使用的Pypi里面的m3u8这个库,可以很快解析m3u8文件内容,同时还使用了pycryptodom这个库中的AES解密(实际上在本次案例中并没有用到)。
# encoding=utf-8
import m3u8
import requests
import datetime
import os
from Crypto.Cipher import AES
from Crypto import Random
import glob
# Request header, not necessary, see website change
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
}def download(ts_urls, download_path, keys=[]):if not os.path.exists(download_path):os.mkdir(download_path)decrypt = Trueif len(keys == 0) or keys[0] is None: # m3u8 will get [None] if not key or []decrypt = Falsefor i in range(len(ts_urls)):ts_url = ts_urls[i]file_name = ts_url.uriprint("start download %s" %file_name)start = datetime.datetime.now().replace(microsecond=0)try:response = requests.get(file_name, stream=True, verify=False)except Exception as e:print(e)returnts_path = download_path+"/{0}.ts".format(i)if decrypt:key = keys[i]iv = Random.new().read(AES.block_size)cryptor = AES.new(key.encode('utf-8'), AES.MODE_CBC)with open(ts_path,"wb+") as file:for chunk in response.iter_content(chunk_size=1024):if chunk:if decrypt:file.write(cryptor.decrypt(chunk))else:file.write(chunk)end = datetime.datetime.now().replace(microsecond=0)print("total time:%s"%(end-start))def merge_to_mp4(dest_file, source_path, delete=False):with open(dest_file, 'wb') as fw:files = glob.glob(source_path + '/*.ts')for file in files:with open(file, 'rb') as fr:fw.write(fr.read())print(f'\r{file} Merged! Total:{len(files)}', end=" ")if delete:os.remove(file)if __name__ == "__main__":url = "test.m3u8"video = m3u8.load(url)print(video.data)download(video.segments, 'tmp', video.keys)merge_to_mp4('result.mp4', 'tmp')
我们将步骤分为两部分,(1)为下载ts文件, (2)为合并ts文件为mp4
(1)下载ts文件
下载ts文件相对简单。根据m3u8库解析出来的地址,我们直接进行request请求获得相应的response。 这时候需要注意的是,如果m3u8解析出来的文件中含有key,说明该文件是通过该key值进行AES加密的,需要对response进行解密后再保存下来,如果没有可以直接保存。
在m3u8库解析m3u8文件中,如果文件不带有key,那么获取到的keys=[None], 它的长度是1,所以不能通过直接判断keys的长度是否为0来决定需不需要解密。
(2)合并成Mp4文件
合并的步骤比较简单,就是将ts按顺序读取之后写到同一个文件里。这边需要注意的是,在保存ts文件时要按照一定的顺序保存,合并时也使用该顺序,否则就会有一种乱序插入的感觉。