惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LINUX DO - 热门话题
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
J
Java Code Geeks
腾讯CDC
博客园 - 聂微东
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
Last Week in AI
Last Week in AI
量子位
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
Google DeepMind News
Google DeepMind News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Schneier on Security
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
有赞技术团队
有赞技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Securelist
AWS News Blog
AWS News Blog
GbyAI
GbyAI
L
LINUX DO - 最新话题
大猫的无限游戏
大猫的无限游戏
Forbes - Security
Forbes - Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
Y
Y Combinator Blog
W
WeLiveSecurity
T
Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Proofpoint News Feed
D
DataBreaches.Net
博客园 - 三生石上(FineUI控件)
V
V2EX
N
News and Events Feed by Topic
Google DeepMind News
Google DeepMind News
D
Docker
The Hacker News
The Hacker News
A
About on SuperTechFans
Security Latest
Security Latest
NISL@THU
NISL@THU
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
博客园_首页
H
Hacker News: Front Page

Yee's Blog

腾讯云/轻量云 安装ArchLinux的个人记录 记录我的联想M720Q Arch Sway窗口管理器的安装和配置过程 Jenkins Linux安装 Ceph块设备对RBD块设备操作LVM创建PV时报错 Ceph映射RBD块设备 2020年9月26日前端部门Vue考核 2020年9月26日后端部门Python考核 Nginx反向代理下载传输超过1G大文件时断开问题 Windows Server 2019 Datacenter M720q 安装记录 Ceph块设备基础及快照、克隆操作 从Ceph集群中删除OSD节点 Centos8配置 chrony NTP服务端及客户端 搭建Ceph集群 Ceph硬件准备 OSD与Pool池的常见操作及管理 Ceph用户管理 MacOS睡眠管理——让你的Mac睡的更香 初中古诗文必背篇目 Ansible 技巧之场 Ansible 包含和导入文件 Ansible 学习文档 Ansible 主机模式 Ansible 角色 Ansible 调整连接数 Splunk数据搜索和报表 Ansible 常用模块 Ansible 变量和事实 Ansible 任务控制 Ansible Playbook Ansible 基础概念 42Team-Flask框架-HTTP协议快速了解 42Team-Flask框架-路由和视图 42Team-Flask框架-请求与响应 42Team-Flask框架 42Team-Flask框架-Flask介绍 42Team-Flask框架-使用Pycharm来构建一个全新的Flask项目 How to install RHEL8(Centos) on Dell R710/R610 server Centos8安装Docker报错解决方案 Centos8安装Docker后容器内无法访问网络 Docker或Podman容器内无法解析DNS问题多种解决方案 使用NetworkManager来管理树莓派Ubuntu系统网络以及个人的体验优化 Linux 存储设备的挂载及识别 Firewalld 防火墙常用指令教程-常用规则(禁止Ping | 放行端口) Linux 查找系统中的文件 Linux 守护进程和控制服务 Linux SSH远程访问 42Team小组_Linux基础培训课件 如何在AmazeUI中当改变select标签后触发Vue方法的问题 使用Docker部署安装WordPress博客平台 在Raspberry树莓派中搭建NFS存储服务器 在 树莓派 Linux 中搭建匿名 danted socks5 代理服务器 HTTP与HTTPS区别随笔 Bash/Shell学习笔记
腾讯课堂、钉钉在线网课视频直播回放下载离线播放
本文作者: YeeFire · 2020-12-28 · via Yee's Blog

[腾讯课堂、钉钉]在线网课视频直播回放下载离线播放

有些精品好课真的是百看不厌、教师精心准备、学习氛围也好。不过有些已购买的课程却只能在线观看…现在讲究的是一个环保、节能。如果能将已购买的课程离线下载好,那么会为各大平台节省多少带宽费用当然还有数据包在这世间重复的传输所浪费的电力资源。

本着环保、节能、减排的目的,开始了尝试对腾讯课堂网页版中自己购买了课程的视频回放进行下载。

然后顺便又看了下钉钉的回放。

github直达项目:https://github.com/yeefire/cloud-class-replay

腾讯课堂

由于没有使用客户端,在网页上观看。使用浏览器中的开发者工具来寻找请求。

tencent_class_safari_devnet-2020-12-28-00-30-43

可以看到请求了很多视频片段,文件拓展名为ts,GET请求。于是直接复制求请求连接然后丢在浏览器地址栏播放,结果不行,是加密的。

因为看到了ts,那么八九不离十使用的是M3U文件存储分段多媒体信息。

ts是日本高清摄像机拍摄下进行的封装格式文件,全称为MPEG2-TS。

M3U8是Unicode版本的M3U,用UTF-8编码。”M3U”和”M3U8”文件都是苹果公司使用的HTTP Live Streaming格式的基础,这种格式可以在iPhone和Macbook等设备播放。

寻找M3U

在请求中搜索m3u,出现了几个m3u8拓展文件,选择资源文件最大的那个m3u8文件,获取cURL请求或者其他方式将其下载到本地方便进一步分析。

现在找到了m3u文件,我们可以获取到这节课的所有分段视频了。

tencent_class_safari_search_m3u8-2020-12-28-00-48-19

分析腾讯课堂M3U文件

已经下载好了m3u8拓展文件,接下来打开文件进行分析!

可以看到腾讯课堂的每个分段视频是使用AES-128进行加密的,好在下载到的m3u8文件里给出了解密密钥的地址以及偏移量。

这下我们有了密钥和偏移量还有分段视频的请求参数(还不知道HTTP请求路径)

tencent_class_m3u8_file-2020-12-28-01-06-24

tencent_class_m3u8_check-2020-12-28-01-11-59

整理总结

现在可以尝试下载一个小的视频片段,不过目前还没有请求视频片段的完整路径,只有一个个的请求参数。这个好办,再回到浏览器中播放回放视频,观察浏览器开发者工具中的网络请求动态,找到’vxxxxxx.ts’请求,并查看获取该视频片段的完整HTTP请求路径。

发现和m3u8的请求路径与其相似。

m3u: https://xxxxxxxxxx.vod2.myqcloud.com/xxxxxxxxxxxxxx/b4e0xxxxxxxxxxxxxx7/drm/voddrm.xxxxxxxxxxxxx

ts: https://xxxxxxxxxx.vod2.myqcloud.com/xxxxxxxxxxxxxx/b4e0xxxxxxxxxxxxxx7/drm/v.fxxxx.ts?start=195027344&end=195666559&type=mpegtsxxxxxxxxxxxxx

在最后出现/斜杠位置前的所有请求路径都相同。并且斜杠后的请求参数正是m3u文件中的一个个分段视频的请求参数,看来仅仅需要简单的拼接就可以将这些分段视频下载好了。

那么现在我们有了全部分段视频的下载请求地址、解密算法、解密密钥及偏移量。有了这些就可以尝试下载分段视频并进行解密和合并了。

先尝试解密一个分段视频试试看:

with aiofiles.open(m3u8_encrypt_file, mode='rb') as f:
f = f.read()
content_video_part = AES.new(key, AES.MODE_CBC, iv).decrypt(f)
with aiofiles.open(dest_decrypt_file, mode='wb') as f:
f.write(content_video_part)

可以正常播放,没有问题。接下来下载全部的分段视频并解密,最后重新整合为一个mp4格式视频文件。剩下的交给脚本处理了!

腾讯课堂回放下载脚本

脚本使用异步进行请求下载分段视频和解密视频,尽可能的以最快的速度下载好全部的分段视频。

如果下载期间遇到网络波动,脚本可以自动重试下载。

若脚本意外停止,可以继续追加下载,不必全部重新开始下载分段视频。

使用方法:

  • 先安装依赖模块 pip3 install pycrypto m3u8 aiofiles requests_async
  • 命令行执行 python3 tencent_class_m3u8.py 这节课的名称 这节课的M3U文件请求地址(网址或者本地路径都可以)

例如: python3 tencent_class_m3u8.py 【Python进阶】Python-上午 https://1dada217.vod2.myqcloud.com/fdadadada3kmdkfsxxxxxxxxxxxxxxxxx

from Crypto.Cipher import AES
import requests_async as requests
import aiofiles
import m3u8
import os, sys
import asyncio

class_video_name = sys.argv[1]
m3u8_file_uri = sys.argv[2]
prefix_request_url = f'{m3u8_file_uri.rsplit("/", 1)[0]}/'


async def download_m3u8_video(index: int, suffix_url: str):
if not os.path.exists(f'{class_video_name}/downloads/{index}.ts'):
i = 0
while i < 3:
try:
download_video_ts = await requests.get(url=prefix_request_url + suffix_url, timeout=30)
with open(f'{class_video_name}/downloads/{index}.ts', "wb") as ts:
ts.write(download_video_ts.content)
print(f'[{class_video_name}]——已下载第 {index} 个片段/ 共 {len(playlist.files)} 个片段')
return
except requests.exceptions.RequestException:
print(f'[{class_video_name}]——下载超时,正在重新下载第 {index} 个片段/ 共 {len(playlist.files)} 个片段')
await asyncio.sleep(i)
i += 1


async def download_m3u8_all():
if not os.path.exists(class_video_name + '/downloads'):
os.makedirs(class_video_name + '/downloads')
download_async_list = [asyncio.create_task(download_m3u8_video(i, video_suffix_url))
for i, video_suffix_url in enumerate(playlist.files, 1)]
await asyncio.wait(download_async_list)

download_encrypt_list = [uri for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.']
if len(download_encrypt_list) == len(playlist.files):
print(f'[{class_video_name}]——视频全部下载完成')
return download_encrypt_list
else:
print(f'[{class_video_name}]——下载过程中出现问题,正在重试...')
return await download_m3u8_all()


async def decrypt_m3u8_video(m3u8_encrypt_file_uri: str, key: bytes, iv: bytes):
decrypt_name = f'{m3u8_encrypt_file_uri.split("/")[-1].split(".")[0]}'
dest_decrypt_uri = f'{class_video_name}/decryption/{decrypt_name}.de.ts'
if not os.path.exists(dest_decrypt_uri):
async with aiofiles.open(m3u8_encrypt_file_uri, mode='rb') as f:
f = await f.read()
content_video_part = AES.new(key, AES.MODE_CBC, iv).decrypt(f)
async with aiofiles.open(dest_decrypt_uri, mode='wb') as f:
await f.write(content_video_part)
print(f'[{class_video_name}]——已解密第 {decrypt_name} 个片段/ 共 {len(playlist.files)} 个片段')


async def decrypt_m3u8_all():
if not os.path.exists(class_video_name + '/decryption'):
os.makedirs(class_video_name + '/decryption')
key = await requests.get(playlist.keys[0].uri)
key = key.content
iv = bytes(playlist.keys[0].iv, 'UTF-8')[:16]
decrypt_m3u8_list = [asyncio.create_task(decrypt_m3u8_video(f'{class_video_name}/downloads/{uri}', key, iv))
for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.']
await asyncio.wait(decrypt_m3u8_list)
print(f'[{class_video_name}]——视频全部解密完成')


def merge_m3u8_all():
download_decrypt_list = [uri for uri in os.listdir(f'{class_video_name}/decryption') if uri[0] != '.']
download_encrypt_list = [uri for uri in os.listdir(f'{class_video_name}/downloads') if uri[0] != '.']
if len(download_decrypt_list) != len(download_encrypt_list):
print('解密分段视频出现问题,可能是受限于类Unix系统文件句柄数量限制导致脚本不能获取足够的文件句柄。\n '
'如果你是 Linux 或 Macos 请尝试在运行本脚本的终端内执行 "ulimit -n 5120" 命令,以解除255(Macos)/1024(Linux)数量限制')
return
with open(f'{class_video_name}/{class_video_name}.mp4', 'ab') as final_file:
print(f'[{class_video_name}]——开始拼接解密后的分段视频')
temp_file_uri_list = os.listdir(f'{class_video_name}/decryption')
temp_file_uri_list.sort(key=lambda x: int(x[:-6]))
for uri in temp_file_uri_list:
if uri[0] == '.': continue
with open(f'{class_video_name}/decryption/{uri}', 'rb') as temp_file:
final_file.write(temp_file.read())
print(f'[{class_video_name}]——合成视频成功')


if __name__ == '__main__':
playlist = m3u8.load(m3u8_file_uri, verify_ssl=False)
del playlist.files[0]
asyncio.run(download_m3u8_all())
asyncio.run(decrypt_m3u8_all())
merge_m3u8_all()
print(f'[{class_video_name}]——视频文件:{os.getcwd()}/{class_video_name}/{class_video_name}.mp4')

钉钉

钉钉回放下载更简单,之后将腾讯课堂回放的脚本稍作删减就可以用于钉钉回放下载。