吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 920|回复: 24
收起左侧

[其他原创] 央视 h5e 视频解密脚本

[复制链接]
xiaoxiij478 发表于 2026-6-13 00:19
本帖最后由 xiaoxiij478 于 2026-6-30 02:22 编辑

众所周知,cctv 上的视频都用了各种奇怪的加密方式,包括 enc / h5e / enc2,所以下载视频需要花费很大力气。
enc2 加密方法现在有"cctv视频下载解密"可以用来解密,但是 h5e 基本上只有浏览器捕捉 / 缓存法可以解密。
所以,我试图将 cctv 网页端的解密算法扒下来,让 h5e 能成为 enc2 用不了的时候的一个备选解密方案。

脚本是开源的,不限制系统,只要能运行 node 就能跑。GitHub 仓库地址:https://github.com/xiaoxi-ij478/cctv-h5e-decrypt

这一次,彻底丢掉 node!现在可以直接在浏览器中解密了:https://cctv-decrypt.xiaoxi-ij478.com
界面比较粗糙,是我自己纯手写的(打)。

最近 h5e 比较老实,没有乱改算法。不过谁知道哪天又变了呢...
使用方法:
这次的更新相比跟上次相比多了一个 worker.js,因为 worker 要求脚本必须在一个单独的文件里。所以在解压的时候务必要注意解压所有文件。

1. 安装 node 环境,可以参考 node 官网。一定要确保你能运行 node!2. 下载 final.tar.gz / final.zip(能解压哪一个就下哪一个),然后找一个文件夹解压
完成后应该有 main.js 和 worker.js,其余以 .map 结尾的是 sourcemap,可以解压出源码,如果你不想要可以删掉
或者也可以直接 clone 上面的仓库,不过要先在仓库文件夹下执行一次 npm i 安装依赖,然后 node main.js 要改成 npx tsx src/cli/main.ts
3. 然后执行以下几个命令:
  • [Shell] 纯文本查看 复制代码
    node main.js --get-guid <码率> <视频网页链接> <输出文件名>
  • [Shell] 纯文本查看 复制代码
    node main.js --get-m3u8 <m3u8 直链> <输出文件名>
  • [Shell] 纯文本查看 复制代码
    node main.js --local-m3u8 <本地 m3u8 文件> <输出文件名>
  • [Shell] 纯文本查看 复制代码
    node main.js <ts 文件> <输出文件名>


其中,视频网页链接是指像 https://tv.cctv.com/2025/10/07/VIDEYQ0ex7y0dZXUmNTvIZUw251007.shtml 这样的。
如果是央视 4K 频道(如上面这个链接),那么码率只有 2000 和 4000 可选,分别对应 720p 和 1080p
其余几乎所有的视频,可选码率有 450, 850, 1200, 2000,都是 720p

本地 m3u8 用法:
对于超大视频(超过 2 GiB),如果想离线解密,直接传 ts 文件,node 会因为文件过大拒绝读取。此时可以利用本地 m3u8 文件来分段解密。
方法:
首先获取到 m3u8 直链,可采用 https://github.com/WeaponJang/get-cntv-guid 所给的油猴脚本来获取。
然后,下载切片,可以上网查询方法。但不要合并切片!
将下载的 m3u8 本身和切片一同放到一个文件夹下(假设 m3u8 指定的切片和 m3u8 在一个目录),然后,调用脚本:
[Shell] 纯文本查看 复制代码
node main.js --local-m3u8 <本地 m3u8 文件> <输出文件名>

即可解密。
效果预览:
Screenshot_2026-06-12_01-41-49.png
Screenshot_2026-06-12_01-44-15.png

v1.1.1 更新:
  • 其实一直以来,下载切片和解密都没能并行,因为解密部分会独占 js 线程导致下载无法继续,这次更新让 node 也用上了 worker。终于可以真正并行了!
  • 增加对外开放的接口:worker wrapper。

v1.1.0 更新:
  • 解密 m3u8 时,所有分片可以在后台并行下载,加快解密速度
  • 初版解密时还需要从央视服务器上下载一个播放器初始化 json,现在 json 已经嵌入在 cctv.worker.js 中,真正做到可以完全离线解密
  • 对外接口增加 util, mpegts 和 nalutil,使调用者可以自行解析 TS 流后传递给解密脚本解密


分享链接:
通过网盘分享的文件:
链接: https://pan.baidu.com/s/1bC12LVRVK6Et0AJKgjZUHw?pwd=tcyx 提取码: tcyx 复制这段内容后打开百度网盘手机App,操作更方便哦

(我也是个 TypeScript 新手,大佬还请轻喷我的代码)
因为原项目有多个源文件,下面只贴上 decrypter.ts 的源代码,这段也是调用 cctv 解密程序的核心:
[JavaScript] 纯文本查看 复制代码
"use strict";

import * as cctvWorkerModule from "./external/cctv.worker.js";

import * as nalutil from "./nalutil.js";
import * as mpegts from "./mpegts.js";
import * as util from "./util.js";
import * as cmdutil from "./cmdutil.js";

export { Decrypter };

// note: this is a synchronous decrypter.
// if you call the decrypt function, your js runtime env will be blocked
// so it is recommended to use this class in a worker
class Decrypter {
    // each of the Decrypter instance will have its own CNTVH5PlayerModule module object
    private CNTVH5PlayerModule = cctvWorkerModule.CNTVModule();
    private shouldDecrypt = false;
    private vmpTag = "";
    private sessionBegin = false;
    private loadFinished: Promise<void>;

    private static readonly pageHost = "https://tv.cctv.com";
    private static readonly mediaTagID = "player_container_player";
    private static readonly MemoryExtend = 2048;

    constructor() {
        this.loadFinished = new Promise(
            resolve =>
                this.CNTVH5PlayerModule.onRuntimeInitialized = () => resolve()
        );
    }

    private __common(o: "InitPlayer" | "UnInitPlayer" | "UpdatePlayer"): number {
        const memory: number = this.CNTVH5PlayerModule._jsmalloc(Decrypter.mediaTagID.length + Decrypter.MemoryExtend);

        this.CNTVH5PlayerModule.HEAP8.fill(0, memory, memory + Decrypter.mediaTagID.length + Decrypter.MemoryExtend);
        this.CNTVH5PlayerModule.HEAP8.set(Array.from(Decrypter.mediaTagID, e => e.charCodeAt(0)), memory);

        let ret: number = 0;
        switch (o) {
            case "InitPlayer":
                ret = this.CNTVH5PlayerModule._CNTV_InitPlayer(memory);
                break;

            case "UnInitPlayer":
                ret = this.CNTVH5PlayerModule._CNTV_UnInitPlayer(memory);
                break;

            case "UpdatePlayer":
                this.vmpTag = this.CNTVH5PlayerModule._CNTV_UpdatePlayer(memory).toString(16).padStart(8, "0");
                break;
        }

        this.CNTVH5PlayerModule._jsfree(memory);
        return ret;
    }
    private InitPlayer(): number   { return this.__common("InitPlayer"); }
    private UnInitPlayer(): number { return this.__common("UnInitPlayer"); }
    private UpdatePlayer(): number { return this.__common("UpdatePlayer"); }

    async beginDecryptSession(): Promise<void> {
        if (this.sessionBegin)
            throw new Error("session already started");

        await this.loadFinished;
        this.sessionBegin = true;
        this.InitPlayer();
        // wait for the json download to complete
        // i know it's very inaccurate
        await new Promise(resolve => setTimeout(resolve, 500));
    }

    endDecryptSession(): void {
        if (!this.sessionBegin)
            throw new Error("session not started yet");

        this.sessionBegin = false;
        this.UnInitPlayer();
    }

    // warning: this function will modify `data` in-place
    decryptNALU(data: nalutil.NALU): nalutil.NALU {
        if (!this.sessionBegin)
            throw new Error("session not started yet");

        this.UpdatePlayer();
        let useSpecialmediaTagID = true;
        switch (data.nalUnitType) {
            case 25:
                this.shouldDecrypt = data.payload[0] === 1;
                useSpecialmediaTagID = false;

                break;

            case 1:
            case 5:
                if (!this.shouldDecrypt)
                    return data;

                break;

            default:
                return data;
        }

        // i just list the keys set by cctv for fun here
        // const StaticCallModuleVod = {
        //    H264NalSet:   (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD7(a, b, c, d),
        //    H265NalData:  (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD6(a, b, c, d),
        //    AVS1AudioKey: (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD5(a, b, c, d),
        //    HEVC2AAC:     (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD4(a, b, c, d),
        //    HASHMap:      (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD3(a, b, c, d),
        //    BASE64Dec:    (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD2(a, b, c, d),
        //    MediaSession: (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD1(a, b, c, d),
        //    Mp4fragment:  (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD0(a, b, c, d),
        //    MpegAudio:    (a, b, c, d) => this.CNTVH5PlayerModule._CNTV_jsdecVOD8(a, b, c, d),
        //    AACDemuxer:   (a, b, c, d) => this.CNTVH5PlayerModule._jsdecVOD(b, c, d)
        //};
        // const StaticCallModuleVodAPI = (a, b, c, d, index) => StaticCallModuleVod[index](a, b, c, d);

        // media tag id cannot be determined after dts and whether it's first session is determined
        // so we switch to use prefix and suffix
        // full format is "myPlayer_player##<dts timestamp>##<seeked ? 1 : 0>
        // (though i was unable to produce 1 on seek)
        const localmediaTagID =
            useSpecialmediaTagID ? `${Decrypter.mediaTagID}##1000000##0` : Decrypter.mediaTagID;

        const addr = this.CNTVH5PlayerModule._jsmalloc(data.payload.byteLength + 1 + Decrypter.MemoryExtend);
        const addr2 = this.CNTVH5PlayerModule._jsmalloc(localmediaTagID.length + 1);

        this.CNTVH5PlayerModule.HEAP8[addr] = data.header;
        this.CNTVH5PlayerModule.HEAP8.set(data.payload, addr + 1);
        this.CNTVH5PlayerModule.HEAP8.set(
            Array.from(Decrypter.pageHost, e => e.charCodeAt(0)), addr + data.payload.byteLength + 1
        );
        this.CNTVH5PlayerModule.HEAP8.set(Array.from(localmediaTagID, e => e.charCodeAt(0)), addr2);

        // how is this function called:
        // if (d && '' != d) for (var m in d) this[r(492)].includes(d[m]) &&
        // this[r(497)](e, p, h, c, l, Object[r(533)](this.StaticCallModuleVod) [m]);
        // f = this.StaticCallModuleVodAPI(e, p, h, c, l, Object[r(533)](this[r(510)]) [8])
        // where:
        // r(492) == StaticCallModuleVodMap == Array.from("0123456")
        // r(497) == StaticCallModuleVodAPI
        // r(533) == keys
        // r(510) == StaticCallModuleVod
        // d == vmpTag
        // e == CNTVH5PlayerModule
        // h == addr
        // p == addr2
        // c == data.payload.byteLength
        // l == pageHost.length
        //
        // in this version i simplified the CNTVH5PlayerModule argument
        for (let i = 0; i < this.vmpTag.length; i++)
            if ("0123456".includes(this.vmpTag[i]))
                this.CNTVH5PlayerModule[
                    `_CNTV_jsdecVOD${7 - i}` as keyof cctvWorkerModule.CNTVModuleType
                ](
                    addr2,
                    addr,
                    data.payload.byteLength + 1,
                    Decrypter.pageHost.length
                );

        const decryptedLength = this.CNTVH5PlayerModule._CNTV_jsdecVOD8(
            addr2,
            addr,
            data.payload.byteLength + 1,
            Decrypter.pageHost.length
        );

        data.reloadData(Uint8Array.from(this.CNTVH5PlayerModule.HEAP8.subarray(addr, addr + decryptedLength)));

        this.CNTVH5PlayerModule._jsfree(addr);
        this.CNTVH5PlayerModule._jsfree(addr2);

        return data;
    }

    // warning: this function will modify `tsFile` in-place
    decryptTsBuffer(tsFile: mpegts.MPEGTS): mpegts.MPEGTS {
        let videoStreamPID = -1;
/*
        // assume there's just one PMT
        for (const stream of tsFile.pmts[0].pmt.streams) {
            if (stream.streamType !== 0x1B)
                continue; // video stream

            videoStreamPID = stream.pid;
            break;
        }

        if (videoStreamPID === -1)
            throw new Error("video stream not found");
*/
        // for now, just assume 0x100 is video PID
        const peses = (tsFile.getPacketsByPID(0x100) as mpegts.MPEGTSPESPacketWithIndex[]);
        for (
            const [i, { pes, indexes }] of
            peses.entries()
        ) {
            const nalus = nalutil.splitNALU(pes.payload!);

            for (const nalu of nalus)
                this.decryptNALU(nalu);

            let newNALU = nalutil.joinNALU(nalus);

            for (const index of indexes) {
                const tsPacket: mpegts.MPEGTSPacket = tsFile.packets[index];
                const offset: number = tsPacket.header.isContinuePacket ? 0 : pes.payloadStartOffset;
                // copy the whole ts packet payload by default
                let bytesToCopy: number = tsPacket.payload!.byteLength;

                if (!tsPacket.payload)
                    continue;

                if (tsPacket.payload.byteLength - offset > newNALU.byteLength) {
                    // this means that the remaining NALU would be smaller than the current TS packet
                    // so we need to adjust adaptation field accordingly

                    if (!tsPacket.adaptationField)
                        tsPacket.adaptationField = new mpegts.MPEGTSPacketAdaptationField;

                    // adaptation field payload length =
                    // 188 (ts packet length)
                    // - 4 (ts packet header)
                    // - 1 (adaptation field length)
                    // - offset (if it is a begin packet then also count pes header)
                    // - newNALU.byteLength (actual data length)
                    tsPacket.adaptationField.payloadLength = 188 - 4 - 1 - offset - newNALU.byteLength;
                    if (tsPacket.adaptationField.payloadLength)
                        tsPacket.adaptationField.payload = util.concatUint8Arrays([
                                // the header (we put nothing inside)
                                Uint8Array.of(0x00),

                                tsPacket.adaptationField.payloadLength > 1 ?
                                    new Uint8Array(tsPacket.adaptationField.payloadLength - 1).fill(0xFF) :
                                    new Uint8Array
                            ]);

                    // set the actual copy length to the nalu length plus the pes header if exists
                    bytesToCopy = newNALU.byteLength + offset;
                }

                tsPacket.payload = util.concatUint8Arrays([
                    // pes header if exists
                    tsPacket.payload.subarray(0, offset),
                    // nalu content
                    newNALU.subarray(0, bytesToCopy - offset)
                ]);
                // remove the copied bytes
                newNALU = newNALU.subarray(bytesToCopy - offset);
            }
        }

        return tsFile;
    }
}



免费评分

参与人数 4吾爱币 +10 热心值 +3 收起 理由
yangqing7145 + 1 + 1 热心回复!
ookk + 1 + 1 谢谢@Thanks!
luobo2233 + 1 用心讨论,共获提升!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

WXJYXLWMH 发表于 2026-6-30 13:35
码率
体育频道 有 蓝光 3000

2025.12.25 cctv.worker.new.js 2159kb 和 main.js 44kb
使用node cpu占用%100 不完美 但下载后的视频是完美的

上个版本 final.js 3119kb 使用node cpu占用下降了一半以上
2026/06/23/VIDEJSB28gcr804v7OoSjela260623.shtml
但下载后的宽屏视频不完美 视频底部的字幕花屏

最新版本 worker.js 3117kb 和 main.js 13kb
使用node cpu占用下降了一半以上
2026/06/23/VIDEJSB28gcr804v7OoSjela260623.shtml
但下载后的宽屏视频不完美 视频底部的字幕花屏

可不可以 旧版本cpu占用问题 和 新版本宽屏视频底部的字幕花屏 都解决一下


没弄明白怎么使用下面这个命令
node main.js --local-m3u8 <*本地* .m3u8 文件> <解密后文件>.ts

如何制作本地 m3u8?
建立一个文件夹,然后获取 m3u8 链接,下载 .m3u8 文件本身和它所指定的所有切片。
不过不要合并切片!只需要下载分片即可!
然后执行 node main.js --local-m3u8 <*本地* .m3u8 文件> <解密后文件>.ts 即可解密。

node main.js --local-m3u8 111.m3u8 video.ts 运行报错
<*本地* .m3u8 文件> 下载后的.n3u8文件内是没有网址链接的 都是1.ts 2.ts...
是无法正常下载.ts文件的

node main.js --local-m3u8 ./ts/111.m3u8 video.ts 运行报错
下载 .m3u8 文件本身和它所指定的所有切片。不过不要合并切片!只需要下载分片即可!
新建文件夹内已经包含111.m3u8原文件和所有.ts文件

是不是可以忽略 如不忽略 那就必须为前缀网址+1.ts 2.ts
基本上用不到 <*本地* .m3u8 文件>
已经支持其他3种方式了

本地 .ts 文件解密 怎么同时解密多个切片?单个.ts 文件解密可以解密。
ookk 发表于 2026-6-29 14:28

(node:18832) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
D:\CCTV\final.js:1498
import * as fsPromises from "node:fs/promises";
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:360:18)
    at wrapSafe (node:internal/modules/cjs/loader:1055:15)
    at Module._compile (node:internal/modules/cjs/loader:1090:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.Module._load (node:internal/modules/cjs/loader:839:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47
苏紫方璇 发表于 2026-6-13 01:29
请在帖子中插入部分关键代码
本版块仅限分享编程技术和源码相关内容,发布帖子必须带上关键代码和具体功能介绍
xinxiu 发表于 2026-6-18 12:48
感谢分享,慢慢研究。
lwgcom 发表于 2026-6-18 14:21
学习了,大佬牛鼻
weiweiguanggao 发表于 2026-6-18 22:03
慢慢,里面都是高手的经验!
zwb83925462 发表于 2026-6-23 17:32
本帖最后由 zwb83925462 于 2026-6-25 12:46 编辑

大佬牛逼,要是有图形化界面就更好了
我整了个WEB UI,已经在GitHub提了issue,劳烦您有空看一下
gxr2010 发表于 2026-6-25 15:50
D:\node>node main.js --get-m3u8 2000 https://dh5wswx02.v.cntv.cn/asp/h5e/hls/main/0303000a/3/default/cd04ecf0f6ab4f4892d4c1dee39f0855/main.m3u8 《百家讲坛》 20260624 食物的历史(第二部) 5 百姓当家菜.ts
CCTV_Woker_BTime 2026-05-12 18:20:40
usage: main.js [--quiet] [--get-m3u8] [--get-guid <resolution>] {in.ts | url} out.ts
这是什么意思啊,
 楼主| xiaoxiij478 发表于 2026-6-25 16:23
gxr2010 发表于 2026-6-25 15:50
D:\node>node main.js --get-m3u8 2000 https://dh5wswx02.v.cntv.cn/asp/h5e/hls/main/0303000a/3/default ...

如果你有 m3u8 直链,就不用加 2000 参数了
顺便说一下,你的这个 m3u8 链接需要改成 https://dh5wswx02.v.cntv.cn/asp/h5e/hls/2000/0303000a/3/default/cd04ecf0f6ab4f4892d4c1dee39f0855/2000.m3u8(因为 main.m3u8 有好多种码率,我的脚本需要明确指定一个下载),最后的文件名要用双引号括起来。
gxr2010 发表于 2026-6-25 17:19
利用github源码终于可以下载了。记录一下:npx tsx D:/node/cctv/src/main.ts --get-m3u8 https://dh5qq01.v.cntv.cn/asp/h5e/hls/2000/0303000a/3/default/cd04ecf0f6ab4f4892d4c1dee39f0855/2000.m3u8 22.mp4
iapeng 发表于 2026-6-29 09:14
记得有人用python写过,现在好像不能下载高分辨率的视频了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-10 18:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表