关注

第二章:基于DrissionPage的M3U8文件解密与视频合并技术详解

第二章:基于DrissionPage的M3U8文件解密与视频合并技术详解

可查看之前博客关于DrissionPage的介绍:第一章、探索DrissionPage:Python下的高效网页自动化与浏览器控制

1、视频m3u8文件介绍

M3U8 文件是一种基于 HTTP 的流媒体播放列表文件,它遵循 HLS(HTTP Live Streaming)协议。HLS 是苹果公司开发的一种流媒体网络传输协议,广泛用于在线视频流的传输,尤其是在 iOS 设备上。M3U8 文件的原理和特点如下:

1.2 文件格式

M3U8 文件本质上是一个文本文件,其扩展名通常是 .m3u8.m3u。文件内容主要包括两部分:

  • 文件头:包含一些元数据,如版本信息、目标持续时间等。
  • 媒体段:列出了一系列媒体文件(通常是 .ts 文件,即 MPEG 传输流文件)的URL,这些文件包含了实际的视频和音频数据。

1.2 工作原理

HLS 协议通过将视频内容切分成一系列小的媒体文件(通常是几秒到十几秒长),并将这些文件的引用放在 M3U8 文件中来工作。当播放器请求流媒体时,服务器会返回这个 M3U8 文件,播放器解析文件并开始下载列表中的媒体文件。

  • 按需下载:播放器根据播放进度下载媒体文件,即只下载当前需要播放的部分,而不是整个视频。
  • 自适应比特率流:M3U8 文件可以包含多个不同质量级别的视频流,播放器可以根据用户的网络条件选择最合适的流进行播放。

1.3 特点

  • 低延迟:HLS 协议允许较低的延迟,尤其是在直播场景中,尽管它通常比实时直播协议的延迟要高。
  • 自适应流:播放器可以根据用户的网络速度和设备性能动态选择不同的视频质量。
  • 兼容性:HLS 广泛支持各种设备和平台,尤其是苹果设备。
  • 易于实现:服务器只需要支持 HTTP/HTTPS 就可以提供 HLS 流,客户端也很容易实现 HLS 播放。

1.4 M3U8 文件示例

一个简单的 M3U8 文件示例如下:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10,
http://example.com/00010.ts
#EXTINF:10,
http://example.com/00020.ts
#EXTINF:10,
http://example.com/00030.ts
...
  • #EXTM3U:文件标识。
  • #EXT-X-VERSION:指定 M3U8 文件的版本。
  • #EXT-X-TARGETDURATION:指定每个媒体段的最大持续时间。
  • #EXT-X-MEDIA-SEQUENCE:指定第一个媒体段的序列号。
  • #EXTINF:指定每个媒体段的持续时间,后面跟着该段的 URL。

1.5 合并 M3U8 文件

由于 M3U8 文件通常包含多个 .ts 文件,如果你想要将这些视频片段合并成一个单一的视频文件(如 .mp4),你可以使用 ffmpeg 这样的工具来实现。

M3U8 文件和 HLS 协议提供了一种灵活、高效的方式来传输和播放视频内容,尤其是在网络条件不稳定的情况下。

1.6 m3u8文件加密的情况

M3U8 文件中的视频内容可以被加密,以保护版权和防止未授权访问。在 HLS(HTTP Live Streaming)流中,有两种主要的加密方法:

  1. AES-128 加密
    这是 HLS 中最常见的加密方法。在这种模式下,每个 .ts 文件或 .m3u8 播放列表文件都可以被单独加密。加密的 .ts 文件通常有一个 .ivf(初始化向量文件)扩展名,但技术上它仍然是一个 .ts 文件。

    在 M3U8 文件中,#EXT-X-KEY 标签用于指定密钥信息,如下所示:

    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:10
    #EXT-X-MEDIA-SEQUENCE:0
    #EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.php?device=iphone",IV=0x1234567890abcdef
    #EXTINF:10,
    http://example.com/encrypted/00010.ts
    #EXTINF:10,
    http://example.com/encrypted/00020.ts
    
    • METHOD=AES-128 指定了加密方法。
    • URI 提供了获取密钥的 URL。
    • IV(初始化向量)是一个唯一的值,用于加密每个 .ts 文件,确保即使相同的内容也会产生不同的加密结果。
  2. 样本加密(Sample Encryption):
    样本加密是对视频和音频样本进行加密,而不是对整个文件进行加密。这种方法可以减少延迟,因为它允许播放器在下载密钥后立即开始解密和播放媒体流。

    在 M3U8 文件中,样本加密可以通过 #EXT-X-BYTERANGE 标签来指定,该标签指示了未加密样本的范围。

  3. DRM 加密(Digital Rights Management):
    除了 AES-128 加密外,HLS 还支持 DRM 加密,如 PlayReady 和 Widevine。这些 DRM 解决方案提供了更高级的保护,但也需要客户端支持相应的 DRM 客户端。

    DRM 加密的 M3U8 文件会包含额外的标签,如 #EXT-X-SESSION-KEY#EXT-X-MAP,用于管理 DRM 会话和指定加密的媒体初始化段。

处理加密的 HLS 流时,播放器需要能够处理加密和解密过程。这通常涉及到与密钥服务器通信以获取解密密钥,然后使用这些密钥来解密下载的 .ts 文件。对于 DRM 加密的内容,还需要播放器具有相应的 DRM 客户端支持。

由于加密和 DRM 解决方案的复杂性,处理加密的 HLS 流通常需要专门的库或服务,这些库或服务能够处理加密和解密的细节,并与 DRM 系统交互。

此处暂时不针对加密的m3u8文件进行处理,后续补充

2、从网页源码中获取m3u8文件的url

通过在某视频网页右键点击“检查”然后通过左上角箭头定位到视频的所在位置然后提取m3u8文件的地址,然后保存即可,之后基于m3u8文件下载视频切片。
在这里插入图片描述

def download_m3u8():
    """1、下载视频的m3u8文件"""
    # 创建ChromiumPage对象
    page = ChromiumPage()

    # 访问包含<video>标签的网页
    page.get('https://xxxxxxxx/vod/detail.html?id=1754117&type_id=726')  # 请替换为实际的URL

    # 定位<video>标签
    video_element = page.ele('tag:video')  # 定位第一个<video>标签
    if video_element:
        # 获取<video>标签内的所有<source>标签
        source_elements = video_element.eles('tag:source')
        for source in source_elements:
            try:
                # 获取每个<source>标签的src属性
                m3u8_url = source.link
                print(m3u8_url)
                # 确保请求成功
                response = requests.get(str(m3u8_url),timeout=5,verify=False)
                if response.status_code == 200:
                    # 打开一个文件用于写入二进制数据
                    with open('playlist_bak.m3u8', 'wb') as file:
                        # 写入请求获取的内容
                        file.write(response.content)
                    print('文件下载成功')
                else:
                    print('文件下载失败,状态码:', response.status_code)
            except AttributeError:
                print(source)
    else:
        print("未找到<video>标签")

3、基于m3u8文件下载视频的.ts切片

m3u8文件的部分数据截图如下所示:刚开始有介绍,这里不多做赘述,这里需要基于这个文件进行提取所有的.ts文件,然后保存到一个文件夹中用于后续合并成一个完整视频;
注意细节:
有的网站里的m3u8文件中的切片地址是完整的,直接根据地址下载即可,有的只有一个切片名,如下图是仅有切片名;

针对这种只有切片名的可以通过浏览器里network点击header查看request URL查看到

在这里插入图片描述

在这里插入图片描述
下面是完整的切片地址,直接下载即可
在这里插入图片描述
然后开始根据m3u8文件下载视频切片
下文所需所有的包

from DrissionPage import ChromiumPage
from DrissionPage import WebPage
import os
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import requests

3.1、采用requests下载

这个方法在某些网站中使用有点不稳定

def process_m3u8_by_requests():
    url = r"https://xxxxxxx/31b39fdb4065/HD/30/2024-11-05/395a7ff0288b/f32228c08395" # 切片的地址
    save_path = r"./video" # 保存目录
    os.makedirs(save_path, exist_ok=True)
    with open('playlist.m3u8', 'r',encoding="utf-8") as file:
        for line in file.readlines():
            # 去除换行符
            line = line.strip("\n")
            # 跳过#号的行
            if line.startswith("#"):
                continue
            else:
                response = requests.get(os.path.join(url,line),verify=False,timeout=5)
                if response.status_code == 200:
                    content = response.content
                    with open(os.path.join(save_path, '{}.ts'.format(line)), 'wb') as f:
                        f.write(content)
                else:
                    print('文件下载失败,状态码:', response.status_code)

3.2、基于DrissionPage下载

def process_m3u8_by_drissionpage():
    url = r"https://xxxxxxx/31b39fdb4065/HD/30/2024-11-05/395a7ff0288b/f32228c08395"
    save_path = r"./video"
    os.makedirs(save_path, exist_ok=True)

    with open('playlist.m3u8', 'r',encoding="utf-8") as file:
        for line in file.readlines():
            # 去除换行符
            line = line.strip("\n")
            # 跳过#号的行
            if line.startswith("#"):
                continue
            else:
                # 保存.ts文件
                page = WebPage('s')
                # 下载文件
                res = page.download(url+r"/"+str(line),save_path,line,show_msg=True,verify=False)
                print(res) # 打印结果

3.2、基于DrissionPage的多线程下载

# 查看程序运行时间
def timing_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 记录开始时间
        result = func(*args, **kwargs)  # 执行函数
        end_time = time.time()  # 记录结束时间
        print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timing_decorator
def multi_thread_download():
    """多线程下载"""
    url = r"https://xxxxxxxxxxx/31b39fdb4065/HD/30/2024-11-05/395a7ff0288b/f32228c08395"
    save_path = r"./video_multi"
    os.makedirs(save_path, exist_ok=True)
    file_ts = []
    with open('playlist.m3u8', 'r',encoding="utf-8") as file:
        for line in file.readlines():
            # 去除换行符
            line = line.strip("\n")
            # 跳过#号的行
            if line.startswith("#"):
                continue
            else:
                file_ts.append(url+r"/"+str(line))
    page = WebPage('s')
    #page.set.download_path(save_path)
    for file_t in file_ts:
        page.download.add(file_t, save_path)
    page.wait.downloads_done()
    page.close()

3.4、异步下载

import asyncio
import aiohttp
import aiofiles
import time
from Crypto.Cipher import AES

def timing_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 记录开始时间
        result = func(*args, **kwargs)  # 执行函数
        end_time = time.time()  # 记录结束时间
        print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timing_decorator
async def download_ts(ts_path, session):
    async with session.get(ts_path) as resp:
        async with aiofiles.open(f"async/{ts_path.split('/')[-1]}", 'wb') as f:
            await f.write(await resp.content.read())
    print(f"{ts_path.split('/')[-1]}下载完毕!")

@timing_decorator
async def drissionPage_download(m3u8_file):
    async with aiohttp.ClientSession() as session:
        tasks = []
        async with aiofiles.open(m3u8_file, 'r', encoding="utf-8") as f:
            async for line in f:
                if line.startswith("#"):
                    continue
                line = line.strip()  # 去掉没用的空格和换行
                task = asyncio.create_task(download_ts(line, session))
                tasks.append(task)
        await asyncio.wait(tasks)

# 主函数,用于启动异步事件循环
async def main():
    await drissionPage_download("playlist.m3u8")

# 运行主函数
if __name__ == "__main__":
	asyncio.run(main())

4、合并.ts或者.jpeg切片数据

要合并多个 .ts 文件成一个视频文件,你可以使用 ffmpeg 工具,它是一个强大的多媒体框架,能够处理视频和音频文件的转换、解码、编码和流处理。以下是使用 ffmpeg 合并 .ts 文件的步骤:

4.1、使用ffmpeg指令合并文件

首先,你需要创建一个文本文件,列出所有要合并的 .ts 文件的路径,每行一个文件路径。这个文件将作为 ffmpeg 的输入。

例如,创建一个名为 file_list.txt 的文件,内容如下:

file 'path/to/video_part1.ts'
file 'path/to/video_part2.ts'
file 'path/to/video_part3.ts'
...

确保替换 'path/to/video_partX.ts' 为你的实际 .ts 文件路径。

使用以下 ffmpeg 命令来合并 .ts 文件:

ffmpeg -f concat -safe 0 -i file_list.txt -c copy output.mp4

这里的命令参数解释如下:

  • -f concat:指定输入文件格式为 concat(连接)。
  • -safe 0:允许文件名包含特殊字符和绝对路径。
  • -i file_list.txt:指定输入文件列表。
  • -c copy:指定复制视频和音频流而不重新编码。
  • output.mp4:指定输出文件的名称。

详细解释:

  • -f concat:这个选项告诉 ffmpeg 输入文件是一个连接格式的文件列表。
  • -safe 0:这个选项允许 ffmpeg 处理包含特殊字符或绝对路径的文件名。如果不设置,ffmpeg 默认只允许相对路径和简单的文件名。
  • -i file_list.txt:指定包含输入文件列表的文本文件。
  • -c copy:这个选项指示 ffmpeg 直接复制视频和音频流,不进行重新编码。这对于合并 .ts 文件特别有用,因为 .ts 文件已经是编码后的视频流,直接复制可以避免重新编码的开销和潜在的质量损失。
  • output.mp4:指定输出文件的名称和格式。你可以根据需要更改输出文件的扩展名,例如 .mkv.avi 等。

示例:

假设你有以下 .ts 文件:

/path/to/video_part1.ts
/path/to/video_part2.ts
/path/to/video_part3.ts

你创建的 file_list.txt 文件内容如下:

file '/path/to/video_part1.ts'
file '/path/to/video_part2.ts'
file '/path/to/video_part3.ts'

然后,运行以下命令:

ffmpeg -f concat -safe 0 -i file_list.txt -c copy output.mp4

这将合并所有列出的 .ts 文件到一个名为 output.mp4 的视频文件中。

注意事项:

  • 确保所有 .ts 文件的编码和封装格式是兼容的,否则合并后的视频可能会有问题。
  • 如果 .ts 文件非常大,合并过程可能需要一些时间。
  • 在某些情况下,你可能需要考虑时间戳和索引的连续性,确保合并后的视频播放流畅。

4.2、使用copy指令合并文件

copy /b video/*.jpeg output.mp4

@timing_decorator
def concat_video(video_path,save_name):
    """
    合并多个ts
    :param video_path:
    :param save_path:
    :return:
    """
    os.system('chcp 65001')  # 将cmd的显示字符编码从默认的GBK改为UTF-8
    os.system(f'copy /b ' + video_path + r'\*.jpeg ' + save_name) # jpeg可以换成ts
    print("合并成功!!!")

5、待更新m3u8解密…

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_43687366/article/details/143840612

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--