1. 背景

通过在 chrome 或其他浏览器的 F12-Network 中发现, 当前网页播放的视频是通过一段一段的 ts 文件加载解析播放的;

python m3u8转成mp4 python ts转mp4_python m3u8转成mp4


如果需要下载该视频, 就需要将所有的 ts 文件下载下来并合并起来;

2. 方法一: cmdcopy /b 指令

ts 文件夹下, 打开 cmd 命令行窗口, 执行 copy /b *.ts target.mp4 命令, 将所有 ts 后缀的文件拷贝合并到目标文件 target.mp4 中即可;
需要注意两点:

2.1. 问题1, 序号列位数不一致

如果 ts 文件的序列号, 不是相同的位数, 即 0-9 是一位序列号, 10-99 是两位序列号, 依次类推, 而不是像上图所示一样, 前面会补零, 保证序列号位数一致;

这种情况下用该命令会导致合并文件时的顺序产生偏差, 最后合并的结果文件无法播放;

python m3u8转成mp4 python ts转mp4_python_02


python m3u8转成mp4 python ts转mp4_python m3u8转成mp4_03

解决方法

可以手动将所有 ts 文件进行前置补零操作, 保证序列号的位数一致, 然后再使用该命令;
或者使用一下的 bat 脚本处理(原知乎作者提供, 本人未验证, 😃 因为直接跳过用方法二来着)

@echo off
set /p n=起始数字:
set /p end=结束数字:
copy %n%.ts out.ts
set num=%n%-%end%
:home
set /a n+=1
echo Y | copy /b out.ts+%n%.ts temp.ts && move /y temp.ts out.ts
if not %n%==%end% goto home
move /y out.ts out_%num%.mp4
pause

2.2. 问题2, 只适用于未加密

该方法只适用于合并后的 ts 文件(可能手动转成其他格式的视频文件) 可以直接播放的, 未经过加密的情况;

视频是否加密的判断, 可以在浏览器的 F12-Network 下面观察, 在第一个 *.ts 文件加载之前, 会提前加载一个 *.m3u8 的文件, 观察该文件的前面几行是否有一行的内容是表示该视频流的加密 key 的, 以下图为例:

python m3u8转成mp4 python ts转mp4_python m3u8转成mp4_04

3. 方法二: 自动化下载 ts 文件以及合并

方法一中, 需要手动下载所有的 ts 文件, 然后进行合并处理, 是一件很繁琐的事情;
该方法则通过 python 编写脚本来自动化处理该过程;
由上面的 2.2 中知道, 一个 ts 视频流加载的最初, 会优先加载一个 m3u8 后缀的文件, 该文件里标注了所有 ts 文件的名称, 以及起始时间位置等其他信息;
所以可以编写脚本, 通过该文件依次下载 ts 文件并进行合并操作;

3.1. 从 m3u8 文件中提取 ts 列表

def get_ts_list(file_path):
    '''
    加载 .m3u8 文件, 返回 ts 列表
    '''
    f = open(file=file_path, mode='r', encoding='UTF-8')
    lines = f.readlines()
    pattern_0 = r'#EXT-X-KEY:METHOD=(.*?),URI="(.*?)"'
    pattern_1 = r'(.*?\.ts)'
    method = ''
    uri = ''
    ts_list = []
    for line in lines:
        matchObj = re.match(pattern_0, line, re.I)
        if matchObj:
            method = matchObj.group(1)
            uri = matchObj.group(2)
            continue
        matchObj = re.match(pattern_1, line, re.I)
        if matchObj:
            ts_list.append(matchObj.group(1))
            continue
    return uri, ts_list

其中 pattern_0 正则表达式是用来匹配获取加密 keyuri 地址的;

3.2. 根据源 m3u8 文件生成本地 m3u8 文件

m3u8 中由两部分需要替换成本地对应文件路径的:

  • 加密 key 文件(#EXT-X-KEY:METHOD=AES-128,URI="(.*?)" URI 后面那一部分), 替换成下载下来的 key 文件;
  • 每个 ts 文件名称, 替换成本地对应的 ts 文件路径;

注意路径的分隔符;

def generate_local_m3u8(src_file, target_file, local_key_file, base_path):
    '''
    根据下载下来的 src_file 文件生成对应的本地 target_file 文件
    其中 key 替换成已下载下来的本地 key 文件路径
    ts 名称替换为本地的 ts 文件路径
    '''
    local_key_file = local_key_file.replace('\\', '/')
    base_path = base_path.replace('\\', '/')
    with open(file=src_file, mode='r', encoding='UTF-8') as f:
        lines = f.readlines()
    pattern_0 = r'#EXT-X-KEY:METHOD=AES-128,URI="(.*?)"'
    pattern_1 = r'(.*?\.ts)'
    new_lines = []
    for line in lines:
        matchObj = re.match(pattern_0, line, re.I)
        if matchObj:
            new_lines.append('#EXT-X-KEY:METHOD=AES-128,URI="' +
                             local_key_file + '"\n')
            continue
        matchObj = re.match(pattern_1, line, re.I)
        if matchObj:
            new_lines.append(base_path + "/" + matchObj.group(1) + "\n")
            continue
        new_lines.append(line)
    with open(target_file, mode='w', encoding='UTF-8') as f:
        f.writelines(new_lines)

3.3. 调用 FFmpeg 合并

使用 pythonffmpeg3 包, 需要准备两步:

  1. 执行 pip install ffmpy3 命令安装 ffmpy3 库;
  2. ffmpeg 官网 下载 windows 版本的压缩包;

下载解压缩后, 配置 环境变量路径 里的 path, 添加解压缩后的文件夹里的 bin 目录到 path 中;

python m3u8转成mp4 python ts转mp4_加载_05


python m3u8转成mp4 python ts转mp4_视频处理_06

然后即可在 python 中使用 FFmpeg(如果没有下载解压缩配置环境变量这一步, 会提示 No Module...异常)

ff = FFmpeg(
    inputs={ '上面生成的本地 m3u8 文件路径': '-allowed_extensions ALL'},
    outputs={ "输出的视频文件.mp4" : '-c copy'})
print(ff.cmd)
ff.run()

注意
这里需要注意一下, 本地的 m3u8 文件里的涉及到的文件路径问题, 包括加密 key 文件路径以及各个 ts 文件的路径, 这些路径可以使用绝对路径(注意分隔符用 /), 也可以使用相对路径, 但是相对路径是相对 m3u8 文件来说的, 而不是相对于你编写的 python 脚本来说的, 切记, 否则执行 FFmpeg 合并文件的时候, 会找不到对应的文件的;

4. 方法三: 插件 猫抓

使用 chrome 扩展插件 猫抓, 源码下载地址

python m3u8转成mp4 python ts转mp4_加载_07

一般可以通过猫抓获取 m3u8 文件, 然后放入自己写的 python 脚本跑下载就可以了~

5. 资源

本文参考来源: https://zhuanlan.zhihu.com/p/94852550