详细记录斗鱼直播源调试过程

博主去年将获取斗鱼直播源的 Python 代码实现放在了 GitHub,期间有几位网友发邮件询问调试过程,但因博主太懒没有一一回复,遂在此做个详细解答并记录成文。

斗鱼直播 PC 网页版的广告非常多,打开时有多卡,想必用过的网友们都深有体会,用广告拦截插件也不太顶用。曾经有个 Chrome 扩展可以替换斗鱼的 H5 播放器,由于开发者放弃更新,已经不能用了。

再后来 Jell 同学在吾爱破解论坛上看到一贴,说可以直接提取出斗鱼直播的真实流媒体地址,放到本地播放器中播放,很流畅而且没有弹幕干扰。在 JavaScript 大神 Jell 的帮助下,博主这个小白也成功调试分析出整个过程,所以本文尽量详细的记录每一步调试操作,不会代码也基本能看懂。

以斗鱼 85894 这个直播间为例,其真实流媒体地址为:https://hdlsa.douyucdn.cn/live/85894rmovieChow_550p.flv?xxxxxxhttps://hlsa.douyucdn.cn/live/live/85894rmovieChow_550/playlist.m3u8?xxxxxx,类似链接可以直接粘贴到 PotPlayer 或 VLC 等流媒体播放器中播放。

博主以下主要用两种不同的方式可获取其直播源:移动端播放页和 PC 端预览页。手机 APP 端暂未调试。PC 端播放页用 WebAssembly 加密搞不太懂,调试不出。

手机网页版播放页

请求参数

Chrome 浏览器 F12 开启调试工具,选择左上角的 Toggle device toolbar 模拟移动设备,打开直播间页面 http://m.douyu.com/85894 。在 “Network” 中找到这个请求 “ http://m.douyu.com/api/room/ratestream ” 的响应数据正是我们需要的:

斗鱼直播源请求地址

整理其请求方式为 POST,参数:

v: 250120200510
did: 4b98044a3696d65bd4f6693300011531
tt: 1589116475
sign: b83ca5afc319dc07512e7454018f0ada
ver: 219032101
rid: 85894
rate: -1

在 Postman 里测试可模拟请求成功,确定不需要带 cookie。初步判断各参数情况:v 是 2501 + 当前年月日;did 和 sign 都是 md5;tt 为 10 位时间戳;rid 为房间号;ver 和 rate 固定不变。

寻找 did 和 sign

查看上步 Request URL 的调用栈或直接全局搜索 “api/room/ratestream”,在 https://shark2.douyucdn.cn/front-publish/m-douyu-v3-master/js/room_3802ffe.js 中找到相关代码:

return function (t, n) {
    var r = n().getIn(["app", "rid"]),
    o = a.getDid(),
    l = parseInt((new Date).getTime() / 1e3, 10);
    try {
        a.post({
            url: "/api/room/ratestream",
            data: window[(0, s.default)(256042, "9f4f419501570ad13334")](r, o, l) + "&ver=" + u.VERSION + "&rid=" + r + "&rate=" + e,
            type: "json"
        })

看 window [(0, s.default)(256042,"9f4f419501570ad13334")](r, o, l),通过下断点,发现这里的 r、o、l 分别是直播间号、did 值和时间戳,为前面 window 方法的传入参数。

斗鱼直播源did值

而 o = a.getDid() 就是生成 did 的函数,继续向上查找到代码块:

t.getDid = function () {
    return (0, o.getStore)().getState().getIn(["app", "did"], "10000000000000000000000000001501")
}

getIn: function (e, t) {
    for (var n, r = this, i = rn(e); !(n = i.next()).done; ) {
        var o = n.value;
        if ((r = r && r.get ? r.get(o, g) : g) === g)
            return t
    }
    return r
}

其他函数可忽略,看 getIn 传入 (["app","did"],"10000000000000000000000000001501"),某种条件下直接 return t 时即返回 10000000000000000000000000001501,这就是 did 的默认值了,后面我们也可验证。

继续找 sign,看 (256042, "9f4f419501570ad13334") 又是传入 s.default 函数的参数。往上查找 s.default:

t.default = function (e, t) {
	for (var n = r.MM(e.toString()).toString(), u = (n.length, n[0].charCodeAt(0)), c = n[16].charCodeAt(0), l = [], f = 0; f < 4; f++)
		l[f] = u << 24 | u << 16 | u << 8 | u, l[f + 4] = c << 24 | c << 16 | c << 8 | c;
	var d = Math.floor(t.length / 16) % 4,
	p = [],
	h = t.length % 8,
	_ = Math.floor(t.length / 8);
	for (f = 0; f < _; f++)
		p[f] = 255 & parseInt(t.substr(8 * f, 2), 16) | parseInt(t.substr(8 * f + 2, 2), 16) << 8 & 65280 | parseInt(t.substr(8 * f + 4, 2), 16) << 24 >>> 8 | parseInt(t.substr(8 * f + 6, 2), 16) << 24;
	var v;
	v = 0 == d ? function (e, t) {
		for (var n = Math.floor(e.length / 2), r = e.slice(0), o = 0; o < n; o++) {
			var a = i(e.slice(2 * o, 2 * o + 2), t.slice(4 * o % 8, 4 * o % 8 + 4));
			r[2 * o + 0] = a[0],
			r[2 * o + 1] = a[1]
		}
		return r
	}
	(p, l) : 1 == d ? function (e, t) {
		for (var n = Math.floor(e.length / 2), r = e.slice(0), i = 0; i < n; i++) {
			var a = o(e.slice(2 * i, 2 * i + 2), 32, t.slice(4 * i % 8, 4 * i % 8 + 4));
			r[2 * i + 0] = a[0],
			r[2 * i + 1] = a[1]
		}
		return r
	}
	(p, l) : function (e, t) {
		for (var n = Math.floor(e.length / 2), r = e.slice(0), i = 0; i < n; i++) {
			var o = a(e.slice(2 * i, 2 * i + 2), 2, t.slice(4 * i % 8, 4 * i % 8 + 4));
			r[2 * i + 0] = o[0],
			r[2 * i + 1] = o[1]
		}
		return r
	}
	(p, l);
	var m = [];
	for (f = 0; f < v.length; f++) { var y = 255 & v[f], g = v[f] >>> 8 & 255,
		b = v[f] >>> 16 & 255,
		A = v[f] >>> 24 & 255;
		y && m.push(y),
		g && m.push(g),
		b && m.push(b),
		A && m.push(A)
	}
	var w = Math.floor(h / 2);
	for (f = 0; f < w; f++)
		m.push(255 & parseInt(t.substr(8 * _ + 2 * f, 2), 16));
	return s.decode(new Uint8Array(m))
}

具体实现过程咱也不去深究,看传参 (256042, "9f4f419501570ad13334") 后的返回值:"ub98484234",即:window ["ub98484234"](r, o, l)。后经验证,目前的返回值是固定为 "ub98484234"。

而 window 的 ub98484234 方法在直播页的 HTML 源码里可以找到,如下(已折叠,点击展开):

function ub98484234(k1b2b12b7a0, k1b2b12b7a1, k1b2b12b7a2) {
	var rk = [22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34, 22, 10, 22, 24, 27, 16, 20, 34, 34];
	var k2 = [0x730217d9, 0xf249f86];
	var lk = [0x730217d9, 0xf249f86];
	var v = k1b2b12b7a.slice(0);
	var k = [0x294823df, 0x18be6784, 0x4ae13d6c, 0x2cd672ae];
	for (var O = 0; O < 374; O++) {
		v[O] ^= 0x50637673;
	}
	v[1] = (v[1] << (lk[1] % 16)) | (v[1] >>> (32 - (lk[1] % 16)));
	v[0] = (v[0] >>> (lk[0] % 16)) | (v[0] << (32 - (lk[0] % 16))); v[3] += lk[1]; v[2] ^= lk[0]; v[5] += lk[1]; v[4] += lk[0]; v[7] ^= lk[1]; v[6] -= lk[0]; v[9] -= lk[1]; v[8] = (v[8] >>> (lk[0] % 16)) | (v[8] << (32 - (lk[0] % 16)));
	v[11] += lk[1];
	v[10] = (v[10] << (lk[0] % 16)) | (v[10] >>> (32 - (lk[0] % 16)));
	v[13] -= lk[1];
	v[12] += lk[0];
	v[15] -= lk[1];
	v[14] ^= lk[0];
	v[17] -= lk[1];
	v[16] -= lk[0];
	v[19] = (v[19] << (lk[1] % 16)) | (v[19] >>> (32 - (lk[1] % 16)));
	v[18] = (v[18] >>> (lk[0] % 16)) | (v[18] << (32 - (lk[0] % 16))); v[21] += lk[1]; v[20] ^= lk[0]; v[23] += lk[1]; v[22] += lk[0]; v[25] ^= lk[1]; v[24] -= lk[0]; v[27] -= lk[1]; v[26] = (v[26] >>> (lk[0] % 16)) | (v[26] << (32 - (lk[0] % 16)));
	v[29] += lk[1];
	v[28] = (v[28] << (lk[0] % 16)) | (v[28] >>> (32 - (lk[0] % 16)));
	v[31] -= lk[1];
	v[30] += lk[0];
	v[33] -= lk[1];
	v[32] ^= lk[0];
	v[35] -= lk[1];
	v[34] -= lk[0];
	v[37] = (v[37] << (lk[1] % 16)) | (v[37] >>> (32 - (lk[1] % 16)));
	v[36] = (v[36] >>> (lk[0] % 16)) | (v[36] << (32 - (lk[0] % 16))); v[39] += lk[1]; v[38] ^= lk[0]; v[41] += lk[1]; v[40] += lk[0]; v[43] ^= lk[1]; v[42] -= lk[0]; v[45] -= lk[1]; v[44] = (v[44] >>> (lk[0] % 16)) | (v[44] << (32 - (lk[0] % 16)));
	v[47] += lk[1];
	v[46] = (v[46] << (lk[0] % 16)) | (v[46] >>> (32 - (lk[0] % 16)));
	v[49] -= lk[1];
	v[48] += lk[0];
	v[51] -= lk[1];
	v[50] ^= lk[0];
	v[53] -= lk[1];
	v[52] -= lk[0];
	v[55] = (v[55] << (lk[1] % 16)) | (v[55] >>> (32 - (lk[1] % 16)));
	v[54] = (v[54] >>> (lk[0] % 16)) | (v[54] << (32 - (lk[0] % 16))); v[57] += lk[1]; v[56] ^= lk[0]; v[59] += lk[1]; v[58] += lk[0]; v[61] ^= lk[1]; v[60] -= lk[0]; v[63] -= lk[1]; v[62] = (v[62] >>> (lk[0] % 16)) | (v[62] << (32 - (lk[0] % 16)));
	v[65] += lk[1];
	v[64] = (v[64] << (lk[0] % 16)) | (v[64] >>> (32 - (lk[0] % 16)));
	v[67] -= lk[1];
	v[66] += lk[0];
	v[69] -= lk[1];
	v[68] ^= lk[0];
	v[71] -= lk[1];
	v[70] -= lk[0];
	v[73] = (v[73] << (lk[1] % 16)) | (v[73] >>> (32 - (lk[1] % 16)));
	v[72] = (v[72] >>> (lk[0] % 16)) | (v[72] << (32 - (lk[0] % 16))); v[75] += lk[1]; v[74] ^= lk[0]; v[77] += lk[1]; v[76] += lk[0]; v[79] ^= lk[1]; v[78] -= lk[0]; v[81] -= lk[1]; v[80] = (v[80] >>> (lk[0] % 16)) | (v[80] << (32 - (lk[0] % 16)));
	v[83] += lk[1];
	v[82] = (v[82] << (lk[0] % 16)) | (v[82] >>> (32 - (lk[0] % 16)));
	v[85] -= lk[1];
	v[84] += lk[0];
	v[87] -= lk[1];
	v[86] ^= lk[0];
	v[89] -= lk[1];
	v[88] -= lk[0];
	v[91] = (v[91] << (lk[1] % 16)) | (v[91] >>> (32 - (lk[1] % 16)));
	v[90] = (v[90] >>> (lk[0] % 16)) | (v[90] << (32 - (lk[0] % 16))); v[93] += lk[1]; v[92] ^= lk[0]; v[95] += lk[1]; v[94] += lk[0]; v[97] ^= lk[1]; v[96] -= lk[0]; v[99] -= lk[1]; v[98] = (v[98] >>> (lk[0] % 16)) | (v[98] << (32 - (lk[0] % 16)));
	v[101] += lk[1];
	v[100] = (v[100] << (lk[0] % 16)) | (v[100] >>> (32 - (lk[0] % 16)));
	v[103] -= lk[1];
	v[102] += lk[0];
	v[105] -= lk[1];
	v[104] ^= lk[0];
	v[107] -= lk[1];
	v[106] -= lk[0];
	v[109] = (v[109] << (lk[1] % 16)) | (v[109] >>> (32 - (lk[1] % 16)));
	v[108] = (v[108] >>> (lk[0] % 16)) | (v[108] << (32 - (lk[0] % 16))); v[111] += lk[1]; v[110] ^= lk[0]; v[113] += lk[1]; v[112] += lk[0]; v[115] ^= lk[1]; v[114] -= lk[0]; v[117] -= lk[1]; v[116] = (v[116] >>> (lk[0] % 16)) | (v[116] << (32 - (lk[0] % 16)));
	v[119] += lk[1];
	v[118] = (v[118] << (lk[0] % 16)) | (v[118] >>> (32 - (lk[0] % 16)));
	v[121] -= lk[1];
	v[120] += lk[0];
	v[123] -= lk[1];
	v[122] ^= lk[0];
	v[125] -= lk[1];
	v[124] -= lk[0];
	v[127] = (v[127] << (lk[1] % 16)) | (v[127] >>> (32 - (lk[1] % 16)));
	v[126] = (v[126] >>> (lk[0] % 16)) | (v[126] << (32 - (lk[0] % 16))); v[129] += lk[1]; v[128] ^= lk[0]; v[131] += lk[1]; v[130] += lk[0]; v[133] ^= lk[1]; v[132] -= lk[0]; v[135] -= lk[1]; v[134] = (v[134] >>> (lk[0] % 16)) | (v[134] << (32 - (lk[0] % 16)));
	v[137] += lk[1];
	v[136] = (v[136] << (lk[0] % 16)) | (v[136] >>> (32 - (lk[0] % 16)));
	v[139] -= lk[1];
	v[138] += lk[0];
	v[141] -= lk[1];
	v[140] ^= lk[0];
	v[143] -= lk[1];
	v[142] -= lk[0];
	v[145] = (v[145] << (lk[1] % 16)) | (v[145] >>> (32 - (lk[1] % 16)));
	v[144] = (v[144] >>> (lk[0] % 16)) | (v[144] << (32 - (lk[0] % 16))); v[147] += lk[1]; v[146] ^= lk[0]; v[149] += lk[1]; v[148] += lk[0]; v[151] ^= lk[1]; v[150] -= lk[0]; v[153] -= lk[1]; v[152] = (v[152] >>> (lk[0] % 16)) | (v[152] << (32 - (lk[0] % 16)));
	v[155] += lk[1];
	v[154] = (v[154] << (lk[0] % 16)) | (v[154] >>> (32 - (lk[0] % 16)));
	v[157] -= lk[1];
	v[156] += lk[0];
	v[159] -= lk[1];
	v[158] ^= lk[0];
	v[161] -= lk[1];
	v[160] -= lk[0];
	v[163] = (v[163] << (lk[1] % 16)) | (v[163] >>> (32 - (lk[1] % 16)));
	v[162] = (v[162] >>> (lk[0] % 16)) | (v[162] << (32 - (lk[0] % 16))); v[165] += lk[1]; v[164] ^= lk[0]; v[167] += lk[1]; v[166] += lk[0]; v[169] ^= lk[1]; v[168] -= lk[0]; v[171] -= lk[1]; v[170] = (v[170] >>> (lk[0] % 16)) | (v[170] << (32 - (lk[0] % 16)));
	v[173] += lk[1];
	v[172] = (v[172] << (lk[0] % 16)) | (v[172] >>> (32 - (lk[0] % 16)));
	v[175] -= lk[1];
	v[174] += lk[0];
	v[177] -= lk[1];
	v[176] ^= lk[0];
	v[179] -= lk[1];
	v[178] -= lk[0];
	v[181] = (v[181] << (lk[1] % 16)) | (v[181] >>> (32 - (lk[1] % 16)));
	v[180] = (v[180] >>> (lk[0] % 16)) | (v[180] << (32 - (lk[0] % 16))); v[183] += lk[1]; v[182] ^= lk[0]; v[185] += lk[1]; v[184] += lk[0]; v[187] ^= lk[1]; v[186] -= lk[0]; v[189] -= lk[1]; v[188] = (v[188] >>> (lk[0] % 16)) | (v[188] << (32 - (lk[0] % 16)));
	v[191] += lk[1];
	v[190] = (v[190] << (lk[0] % 16)) | (v[190] >>> (32 - (lk[0] % 16)));
	v[193] -= lk[1];
	v[192] += lk[0];
	v[195] -= lk[1];
	v[194] ^= lk[0];
	v[197] -= lk[1];
	v[196] -= lk[0];
	v[199] = (v[199] << (lk[1] % 16)) | (v[199] >>> (32 - (lk[1] % 16)));
	v[198] = (v[198] >>> (lk[0] % 16)) | (v[198] << (32 - (lk[0] % 16))); v[201] += lk[1]; v[200] ^= lk[0]; v[203] += lk[1]; v[202] += lk[0]; v[205] ^= lk[1]; v[204] -= lk[0]; v[207] -= lk[1]; v[206] = (v[206] >>> (lk[0] % 16)) | (v[206] << (32 - (lk[0] % 16)));
	v[209] += lk[1];
	v[208] = (v[208] << (lk[0] % 16)) | (v[208] >>> (32 - (lk[0] % 16)));
	v[211] -= lk[1];
	v[210] += lk[0];
	v[213] -= lk[1];
	v[212] ^= lk[0];
	v[215] -= lk[1];
	v[214] -= lk[0];
	v[217] = (v[217] << (lk[1] % 16)) | (v[217] >>> (32 - (lk[1] % 16)));
	v[216] = (v[216] >>> (lk[0] % 16)) | (v[216] << (32 - (lk[0] % 16))); v[219] += lk[1]; v[218] ^= lk[0]; v[221] += lk[1]; v[220] += lk[0]; v[223] ^= lk[1]; v[222] -= lk[0]; v[225] -= lk[1]; v[224] = (v[224] >>> (lk[0] % 16)) | (v[224] << (32 - (lk[0] % 16)));
	v[227] += lk[1];
	v[226] = (v[226] << (lk[0] % 16)) | (v[226] >>> (32 - (lk[0] % 16)));
	v[229] -= lk[1];
	v[228] += lk[0];
	v[231] -= lk[1];
	v[230] ^= lk[0];
	v[233] -= lk[1];
	v[232] -= lk[0];
	v[235] = (v[235] << (lk[1] % 16)) | (v[235] >>> (32 - (lk[1] % 16)));
	v[234] = (v[234] >>> (lk[0] % 16)) | (v[234] << (32 - (lk[0] % 16))); v[237] += lk[1]; v[236] ^= lk[0]; v[239] += lk[1]; v[238] += lk[0]; v[241] ^= lk[1]; v[240] -= lk[0]; v[243] -= lk[1]; v[242] = (v[242] >>> (lk[0] % 16)) | (v[242] << (32 - (lk[0] % 16)));
	v[245] += lk[1];
	v[244] = (v[244] << (lk[0] % 16)) | (v[244] >>> (32 - (lk[0] % 16)));
	v[247] -= lk[1];
	v[246] += lk[0];
	v[249] -= lk[1];
	v[248] ^= lk[0];
	v[251] -= lk[1];
	v[250] -= lk[0];
	v[253] = (v[253] << (lk[1] % 16)) | (v[253] >>> (32 - (lk[1] % 16)));
	v[252] = (v[252] >>> (lk[0] % 16)) | (v[252] << (32 - (lk[0] % 16))); v[255] += lk[1]; v[254] ^= lk[0]; v[257] += lk[1]; v[256] += lk[0]; v[259] ^= lk[1]; v[258] -= lk[0]; v[261] -= lk[1]; v[260] = (v[260] >>> (lk[0] % 16)) | (v[260] << (32 - (lk[0] % 16)));
	v[263] += lk[1];
	v[262] = (v[262] << (lk[0] % 16)) | (v[262] >>> (32 - (lk[0] % 16)));
	v[265] -= lk[1];
	v[264] += lk[0];
	v[267] -= lk[1];
	v[266] ^= lk[0];
	v[269] -= lk[1];
	v[268] -= lk[0];
	v[271] = (v[271] << (lk[1] % 16)) | (v[271] >>> (32 - (lk[1] % 16)));
	v[270] = (v[270] >>> (lk[0] % 16)) | (v[270] << (32 - (lk[0] % 16))); v[273] += lk[1]; v[272] ^= lk[0]; v[275] += lk[1]; v[274] += lk[0]; v[277] ^= lk[1]; v[276] -= lk[0]; v[279] -= lk[1]; v[278] = (v[278] >>> (lk[0] % 16)) | (v[278] << (32 - (lk[0] % 16)));
	v[281] += lk[1];
	v[280] = (v[280] << (lk[0] % 16)) | (v[280] >>> (32 - (lk[0] % 16)));
	v[283] -= lk[1];
	v[282] += lk[0];
	v[285] -= lk[1];
	v[284] ^= lk[0];
	v[287] -= lk[1];
	v[286] -= lk[0];
	v[289] = (v[289] << (lk[1] % 16)) | (v[289] >>> (32 - (lk[1] % 16)));
	v[288] = (v[288] >>> (lk[0] % 16)) | (v[288] << (32 - (lk[0] % 16))); v[291] += lk[1]; v[290] ^= lk[0]; v[293] += lk[1]; v[292] += lk[0]; v[295] ^= lk[1]; v[294] -= lk[0]; v[297] -= lk[1]; v[296] = (v[296] >>> (lk[0] % 16)) | (v[296] << (32 - (lk[0] % 16)));
	v[299] += lk[1];
	v[298] = (v[298] << (lk[0] % 16)) | (v[298] >>> (32 - (lk[0] % 16)));
	v[301] -= lk[1];
	v[300] += lk[0];
	v[303] -= lk[1];
	v[302] ^= lk[0];
	v[305] -= lk[1];
	v[304] -= lk[0];
	v[307] = (v[307] << (lk[1] % 16)) | (v[307] >>> (32 - (lk[1] % 16)));
	v[306] = (v[306] >>> (lk[0] % 16)) | (v[306] << (32 - (lk[0] % 16))); v[309] += lk[1]; v[308] ^= lk[0]; v[311] += lk[1]; v[310] += lk[0]; v[313] ^= lk[1]; v[312] -= lk[0]; v[315] -= lk[1]; v[314] = (v[314] >>> (lk[0] % 16)) | (v[314] << (32 - (lk[0] % 16)));
	v[317] += lk[1];
	v[316] = (v[316] << (lk[0] % 16)) | (v[316] >>> (32 - (lk[0] % 16)));
	v[319] -= lk[1];
	v[318] += lk[0];
	v[321] -= lk[1];
	v[320] ^= lk[0];
	v[323] -= lk[1];
	v[322] -= lk[0];
	v[325] = (v[325] << (lk[1] % 16)) | (v[325] >>> (32 - (lk[1] % 16)));
	v[324] = (v[324] >>> (lk[0] % 16)) | (v[324] << (32 - (lk[0] % 16))); v[327] += lk[1]; v[326] ^= lk[0]; v[329] += lk[1]; v[328] += lk[0]; v[331] ^= lk[1]; v[330] -= lk[0]; v[333] -= lk[1]; v[332] = (v[332] >>> (lk[0] % 16)) | (v[332] << (32 - (lk[0] % 16)));
	v[335] += lk[1];
	v[334] = (v[334] << (lk[0] % 16)) | (v[334] >>> (32 - (lk[0] % 16)));
	v[337] -= lk[1];
	v[336] += lk[0];
	v[339] -= lk[1];
	v[338] ^= lk[0];
	v[341] -= lk[1];
	v[340] -= lk[0];
	v[343] = (v[343] << (lk[1] % 16)) | (v[343] >>> (32 - (lk[1] % 16)));
	v[342] = (v[342] >>> (lk[0] % 16)) | (v[342] << (32 - (lk[0] % 16))); v[345] += lk[1]; v[344] ^= lk[0]; v[347] += lk[1]; v[346] += lk[0]; v[349] ^= lk[1]; v[348] -= lk[0]; v[351] -= lk[1]; v[350] = (v[350] >>> (lk[0] % 16)) | (v[350] << (32 - (lk[0] % 16)));
	v[353] += lk[1];
	v[352] = (v[352] << (lk[0] % 16)) | (v[352] >>> (32 - (lk[0] % 16)));
	v[355] -= lk[1];
	v[354] += lk[0];
	v[357] -= lk[1];
	v[356] ^= lk[0];
	v[359] -= lk[1];
	v[358] -= lk[0];
	v[361] = (v[361] << (lk[1] % 16)) | (v[361] >>> (32 - (lk[1] % 16)));
	v[360] = (v[360] >>> (lk[0] % 16)) | (v[360] << (32 - (lk[0] % 16))); v[363] += lk[1]; v[362] ^= lk[0]; v[365] += lk[1]; v[364] += lk[0]; v[367] ^= lk[1]; v[366] -= lk[0]; v[369] -= lk[1]; v[368] = (v[368] >>> (lk[0] % 16)) | (v[368] << (32 - (lk[0] % 16)));
	v[371] += lk[1];
	v[370] = (v[370] << (lk[0] % 16)) | (v[370] >>> (32 - (lk[0] % 16)));
	v[373] -= lk[1];
	v[372] += lk[0];
	for (var I = 0; I < 374; I += 2) {
		var i,
		v0 = v[I] ^ k2[0],
		v1 = v[I + 1] ^ k2[1],
		d = 0x9E3779B9,
		sum = d * rk[I / 2];
		for (i = 0; i < rk[I / 2]; i++) {
			v1 -= (((v0 << 4) ^ (v0 >>> 5)) + v0) ^ (sum + k[(sum >>> 11) & 3]);
			sum -= d;
			v0 -= (((v1 << 4) ^ (v1 >>> 5)) + v1) ^ (sum + k[sum & 3]);
		}
		v[I] = v0 ^ k2[1];
		v[I + 1] = v1 ^ k2[0];
	}
	for (var O = 373; O > 0; O--) {
		v[O] ^= v[O - 1];
	}
	v[0] ^= 0x50637673;
	var strc = "";
	for (var i = 0; i < v.length; i++) { strc += String.fromCharCode(v[i] & 0xff, v[i] >>> 8 & 0xff, v[i] >>> 16 & 0xff, v[i] >>> 24 & 0xff);
	}
	return eval(strc)(k1b2b12b7a0, k1b2b12b7a1, k1b2b12b7a2);
}
var k1b2b12b7a = [0xc2dd278c, 0x855ba57e, 0x86c7a49d, 0x3719089e, 0xd23a7843, 0xf12db9d8, 0x21f16117, 0x956720db, 0xefbfc9b2, 0x2f364020, 0x74fa9bbe, 0xa15526ee, 0x11843049, 0xbc390be2, 0x5cc366ec, 0xd3a26be9, 0x1493ab1e, 0x837ded17, 0x551d462f, 0x97127eb2, 0xd86abcea, 0xd81ee068, 0x73950ab4, 0x812c1c23, 0x624ecbc0, 0xab2197b8, 0xffe03fd1, 0x9478dd9c, 0x8eeff367, 0x662f896c, 0x89eaa863, 0x587f9e88, 0x69e8e89f, 0xcb943041, 0xec6b7cfb, 0x30272e06, 0x7dd60de9, 0xac9bced5, 0x162739cf, 0x457cc1e4, 0xa15d9b0, 0x77714909, 0x76ebff8d, 0xc59749a4, 0x9e6d0323, 0x50c7e915, 0x78288844, 0xe44dbd2c, 0xc66c6b, 0xeb9d29f1, 0xabe12249, 0xca71ad9d, 0xe8d44c96, 0x158670e7, 0x5950f035, 0x8d8d7cbd, 0xce6369f7, 0x88069f56, 0xcb1f23ac, 0xe0b22d1e, 0x88f81fad, 0xec887ae, 0xcb8c96c5, 0xd2147535, 0x3c351d61, 0x2be205c8, 0xecf1d6c2, 0x27f01a5f, 0xa0583dad, 0x7e0f48b6, 0x614c5ad4, 0x2657f4f8, 0x620c844a, 0x244c8356, 0x7a53f51c, 0x937dcc64, 0xedb044c1, 0x81446fed, 0xa3f81fd0, 0x761989f6, 0x7a48c39d, 0x73fe065f, 0x3d1cd52a, 0x9dfbf54a, 0x3e5ee575, 0xc7086091, 0xe2c57c35, 0x17c7070b, 0xabbfdee6, 0x38eb46ae, 0xc3f98d90, 0x43f4960, 0x605f037d, 0x922cd5ea, 0x47638ceb, 0x54b6f08, 0x4fd6ead, 0x9338bf7b, 0x2a1a732c, 0x652260af, 0x96f87ed5, 0xf80bb722, 0xe415453b, 0x963e011b, 0x1a39e34f, 0x37cd6b81, 0xdc2eb07, 0x25f75747, 0xc265ae1, 0x5e63e7b3, 0xe8d73586, 0xd6e0835, 0x125f702d, 0x327cdf98, 0x27ab1b69, 0x2dc4f54, 0xd2e7d628, 0xdb66c404, 0x750e2249, 0x67c523cb, 0x94fb776b, 0xfd7fdf9f, 0x79a3945d, 0xf010b287, 0x1e12fd01, 0x35667aae, 0x83676a61, 0xc25254e8, 0x56e305b2, 0x7c16394e, 0x277151c6, 0xaabb503c, 0xb986603a, 0x7c1cf7ad, 0xc964c1f4, 0x28073527, 0x779f944d, 0x4b74cbf8, 0xb9bf8596, 0xf52f555d, 0x139feda8, 0x62b24cc5, 0x8b68fa1c, 0x6be911e9, 0x298cef2c, 0x7adcc1d9, 0x35877847, 0xe6283bc4, 0xaa40acd9, 0xa749c9, 0x16656d6c, 0xfe99f490, 0xd05cc757, 0x3b7e6d85, 0xbe5f50b8, 0x99c3dd41, 0xcc318227, 0xb593787f, 0x118e680d, 0xab944e0b, 0x6ad802d2, 0x31f23543, 0xd4e378cc, 0xd60b22f9, 0x4d4744f2, 0xa9dff4e0, 0xef62940e, 0xb0e6c7d9, 0x3602cef0, 0xd4364c65, 0xc69691f1, 0x451e57ae, 0x484cf621, 0x5364d74f, 0xb058bf45, 0x329596e5, 0x7e705d5b, 0xa7d26cb1, 0x5cc2533b, 0x36903c2a, 0x61be1176, 0x5b3c7152, 0xdba050ac, 0x98a47ac3, 0xe6fe392a, 0xe09aba8e, 0x6198b451, 0xcebe7854, 0xbd8750f0, 0xf29dfdfa, 0x70905320, 0x573b98ce, 0xb43c82d8, 0x48efcc5e, 0x1c4a7662, 0x6ac89fde, 0xc69e2b50, 0xd6caf124, 0xd95da6a, 0x507df9b7, 0xd51ab8e6, 0xb8aca859, 0x671cec21, 0xc785f946, 0x5487041d, 0xfd2ad85b, 0xc5fe18f3, 0xb6da566b, 0x263892b3, 0x3ccc3ade, 0xf2095d00, 0x61a6242f, 0x50092d53, 0x59fe3500, 0x31d9fda0, 0x41b64e49, 0xb478846c, 0xc5129c4f, 0x77d8c162, 0xc56257d5, 0x989b7f26, 0xe98a10cf, 0x9fd03dc3, 0x5d8a4c9b, 0xb0c5a48, 0x33174b73, 0xeaeb0345, 0x3c7a4693, 0xf1e35cc6, 0x7c97cacd, 0x8cab8f08, 0x6bcf5d11, 0xaaf5ed2b, 0xefc34620, 0xdb24a1b2, 0x7cf78307, 0xe7de819, 0x56dc89e0, 0xb21f1225, 0xbffcf9e4, 0xa0d1062c, 0x5b37eb4d, 0xfc01c52c, 0x3f374e7a, 0x1a9d428b, 0x5af3461c, 0x930fcc4b, 0x7123619a, 0x234b0171, 0x283e901c, 0x66bb4071, 0x55e4d8a4, 0xa3c10387, 0x51554619, 0xd826e773, 0xf4581463, 0xf0db9940, 0xb6ace2c2, 0xe9bc520c, 0xeb22a6d7, 0x9d37c24, 0xa70eebb, 0x556c62df, 0xcfe5b5a0, 0xc59c3c83, 0xee133631, 0xc111ea7e, 0xe333af56, 0xd2700259, 0x12be9a93, 0x38925a12, 0xaabdf116, 0xbc8e9555, 0x4efd55d4, 0xece7f684, 0xac656f2c, 0xdd3387be, 0x46234e9b, 0xb1f1c3ce, 0x37f000d, 0x661fddef, 0xe8314dd9, 0x8728a2fc, 0x34b862fd, 0x27eb17c0, 0xe886d8af, 0x4f55fbe1, 0x968d4953, 0x6282804, 0xa625ed8e, 0x9342c3e, 0x5f99bf15, 0x7e5fcd29, 0x23bc88a0, 0x26d7ee7b, 0x22a2cbc2, 0x8b7a0bd, 0x3c9a46a1, 0xd82d68c, 0x515b44f2, 0xd5051214, 0x548d5162, 0x7cacd34b, 0x7e5c8cde, 0x830346a2, 0xc3abc892, 0xc5833350, 0x7595ba8b, 0x7ca749cc, 0x23e60e71, 0xcad97964, 0x8b183a56, 0x5d9f2e2c, 0xc2b2bed9, 0x69c2f5cc, 0xec8d38c7, 0xb2e6ac94, 0x3f4b40ea, 0xf02e958, 0x96aa6259, 0x4e2dd63, 0x64541fda, 0xb67a4b1c, 0xcf1c8409, 0xceb33fc1, 0xe54ed6c9, 0xd909990d, 0xab854142, 0x9297f421, 0x7fcff2d7, 0xfe84a8e, 0x5e10b945, 0xff0c39c9, 0xc3d5f5c0, 0xaec5069d, 0x342bf802, 0xc215f0c5, 0xd3baa4d7, 0x7b1893b9, 0x55a0330, 0xc58f0fde, 0x3d3b4133, 0x73245f39, 0x9121d62, 0x73ac22a8, 0x458d4fd, 0xe5d5b2ab, 0x154fb8a6, 0x3f9139a4, 0x93bd4896, 0x5b197b9, 0x97160400, 0x2e663c8b, 0xdbc92897, 0xef6c649c, 0xdae7eb2c, 0xb9b54bc6, 0xc9bd3f28, 0xdc82cd36, 0xdec82971, 0xa6314d9b, 0x2b7b406c, 0x81f56b81, 0x66db59a4, 0xf1ff72f7, 0x9a58b14f, 0x2c1dd288, 0x8b4a8f93, 0xa7c2222c, 0x210131eb, 0xfeaf5ccf, 0x11a34e24, 0x3950bcb9, 0x4d1ece74];

其中 k1b2b12b7a0, k1b2b12b7a1, k1b2b12b7a2 为 room_3802ffe.js 传入的 r、o、l。里面各种位运算后,使用 fromCharCode() 方法返回 v 数组编码后的字符串 strc,strc 应该是一个函数,最后 eval() 执行带上 r、o、l 参数的 strc 函数并返回。

斗鱼直播源sign值

断点后看到strc的代码如下:

(function (xx0, xx1, xx2) {
    var cb = xx0 + xx1 + xx2 + "250120200510";
    var rb = CryptoJS.MD5(cb).toString();
    var re = [];
    for (var i = 0; i < rb.length / 8; i++)
        re[i] = (parseInt(rb.substr(i * 8, 2), 16) & 0xff) | ((parseInt(rb.substr(i * 8 + 2, 2), 16) << 8) & 0xff00) | ((parseInt(rb.substr(i * 8 + 4, 2), 16) << 24) >>> 8) | (parseInt(rb.substr(i * 8 + 6, 2), 16) << 24);
    var k2 = [0x7b8d6154, 0x520c93b2, 0x23c20, 0x660fa3c0];
    for (var I = 0; I < 2; I++) {
        var v0 = re[I * 2],
        v1 = re[I * 2 + 1],
        sum = 0,
        i = 0;
        var delta = 0x9e3779b9;
        for (i = 0; i < 32; i++) {
            sum += delta;
            v0 += ((v1 << 4) + k2[0]) ^ (v1 + sum) ^ ((v1 >>> 5) + k2[1]);
            v1 += ((v0 << 4) + k2[2]) ^ (v0 + sum) ^ ((v0 >>> 5) + k2[3]);
        }
        re[I * 2] = v0;
        re[I * 2 + 1] = v1;
    }
    re[0] = (re[0] << (k2[0] % 16)) | (re[0] >>> (32 - (k2[0] % 16)));
    re[0] = (re[0] >>> (k2[2] % 16)) | (re[0] << (32 - (k2[2] % 16))); re[0] = (re[0] >>> (k2[0] % 16)) | (re[0] << (32 - (k2[0] % 16)));
    re[0] = (re[0] << (k2[2] % 16)) | (re[0] >>> (32 - (k2[2] % 16)));
    re[0] += k2[2];
    re[1] ^= k2[1];
    re[1] -= k2[3];
    re[1] -= k2[1];
    re[1] ^= k2[3];
    re[2] -= k2[0];
    re[2] -= k2[2];
    re[2] += k2[2];
    re[3] += k2[1];
    re[3] ^= k2[3];
    re[3] ^= k2[1];
    re[3] += k2[3];
    re[0] = (re[0] << (k2[0] % 16)) | (re[0] >>> (32 - (k2[0] % 16)));
    re[0] += k2[2];
    re[0] ^= k2[2];
    re[1] = (re[1] >>> (k2[1] % 16)) | (re[1] << (32 - (k2[1] % 16))); re[1] -= k2[3]; re[1] = (re[1] >>> (k2[3] % 16)) | (re[1] << (32 - (k2[3] % 16)));
    re[1] ^= k2[3];
    re[2] -= k2[0];
    re[2] += k2[2];
    re[2] -= k2[2];
    re[2] ^= k2[2];
    re[3] ^= k2[1];
    re[3] += k2[3];
    re[3] ^= k2[3];
    re[3] ^= k2[3]; {
        var hc = '0123456789abcdef'.split('');
        for (var i = 0; i < re.length; i++) {
            var j = 0,
            s = '';
            for (; j < 4; j++) s += hc[(re[i] >> (j * 8 + 4)) & 15] + hc[(re[i] >> (j * 8)) & 15];
            re[i] = s;
        }
        re = re.join('');
    }
    var rt = "v=250120200510" + "&did=" + xx1 + "&tt=" + xx2 + "&sign=" + re;
    return rt;
});;

里面又是各种位运算,其中 xx0, xx1, xx2 分别是房间号、did 和时间戳。CryptoJS.MD5(cb) 是著名的前端加密控件 CryptoJS 的 MD5 方法。re 即我们千辛万苦在找的 sign 值。最后返回的 rt 为 "v=250120200510" + "&did=" + xx1 + "&tt=" + xx2 + "&sign=" + re 这种形式的字符串,拼接上 room_3802ffe.js 中的 "&ver=" + u.VERSION + "&rid=" + r + "&rate=" + e 后,即 POST 所需的完整参数了。

PC 网页版列表页预览

 请求参数

1、在斗鱼 PC 网页的直播列表页,比如 https://www.douyu.com/directory/all,使用 Chrome 等 “先进” 浏览器,鼠标移到任一直播间图片上会显示视频预览,此时 Chrome 开发者工具 Network 中也可看到加载的流媒体地址,不过并非所有直播间都有此预览,应该是热门直播间才有的权限。

2、Network 里也看到一条 POST 数据,地址形式:https://playweb.douyucdn.cn/lapi/live/hlsH5Preview/85894?158913924910168860,请求参数 rid 和 did。rid 是房间号。而 did 在移动网页版的调试过程中已找到缺省值 "10000000000000000000000000001501"。

在 Postman 中测试几次后,确定请求头中还有三个必要参数:auth: 8198e32c862193b93af402a7f8c6897e、rid: 85894 和 time: 1589139266463。其中 rid 是房间号,time 是当前 13 位时间戳,所以只要找到 auth 即可,而且它看起来也像是 32 位的 MD5 值。

寻找 auth

1、 在 Chrome 开发者工具中全局搜索 hlsH5Preview 或用链接下断点,找到 common~21833f8f_xxx.js 中执行预览操作时 POST 相关代码块,如下:

ListCommonServices.prototype.getVideoHls = function getVideoHls(e, t) {
    var r = this.cookie.get("did") || "",
    o = (new Date).getTime(),
    n = parseInt(o.toString() + parseInt(1e5 * Math.random(), 10).toString(), 10),
    i = l()(String(e) + o).toString(),
    a = g + "lapi/live/hlsH5Preview/" + e + "?" + n,
    c = {
        rid: e,
        time: o,
        auth: i,
        "Content-Type": "application/x-www-form-urlencoded"
    },
    u = {
        rid: e,
        did: r
    }

解析斗鱼直播源auth值

其中参数 r 就是 did,是从 cookie 中得到的;参数 i 即 auth,往函数 l() 中传入一个 String (e) 和 o 拼接参数后的返回值,e 是 rid,o 是 (new Date).getTime() 得到的时间戳,继续查找 l(),下断点后可以在 common~2a42e354_xxx.js 中找到 MD5 函数:

    e.MD5 = o._createHelper(c),
    e.HmacMD5 = o._createHmacHelper(c)
}(Math),
r.MD5)

2、当然不用管这个函数的具体内容,只需验证是否正确即可,拼接房间号 85894 和时间戳 1589139266463 两个字符串即 858941589139266463,再 MD5 加密结果71d7707ca737e1a5b3db6b16e450dc9a,和上图中 i 的值一样。

3、看上面第三步的 var r = this.cookie.get("did") ||"",虽然有个或运算,但 did 是不能为空的,是从 cookie 里来的,继续单步调试发现是获取 cookie 中的 afc_did 字段。既然我们已经知道了 did 的默认值,此处就不在纠结。

get: function get(e) {
var t = n.keyPre ? n.keyPre + e : e
   , r = document.cookie.match(new RegExp("(^| )" + t + "=([^;]*)(;|$)"));
return null !== r ? decodeURIComponent(r[2]) : null
}

PC 网页版播放页

调试过程

PC 网页版打开 http://www.douyu.com/85894,找到的请求地址和参数是这样的:

Request URL:https://www.douyu.com/lapi/live/getH5Play/85894,Request Method: POST,参数:cdn=cmcc,&rate=2,&ver=Douyu_219112905,&iar=0,&ive=1,&ct=0,&cr=0,&tr=8,&q=a2d11f7d05f36b8dfb2bc8d92529f446756c0c114f5973b60032d7b727045f2b4b02b7d225f7d617e02549006ef6fac5796843ad4bcff03a13e176d2bc2a87a4b598998e1cdd47b689c78f50af1fe8938893928b09d23a46fafda5398becbc3da983b788f84ae2288e5f3a3c75486fad3dd168232eca7d3dd156db73cf951ec5e9e7c971bfdb3decccd64339fe6546ffb29852c04b8298976ca5d0778435a39c,&e=0C6fcf2029a2972185bcc6640a1bcf84cbf46be0c01d4e16a6d6820d0a71cee634c4aa54b1f50672b9ce6cfc0d33564e77fac324e24900cdfeb882e2e3d07d32b54fbbc48f3ed260e56080638facb085871c38bff4a9537b4d7720cd9dd738811007237a4190d87f57dc572c29b703f7245861ca0b9bcf12632950895bd71eddde2877f37838fa7920af7142ab66a29576ba60f6e66d3bd83ed22ba1cf13167b7a7989efc03a08b07731114915d254ef77c5c69d9d4a9565aeedd6613ad76447333c157588ac847f382b561b19fe681f71e6373bde1e644739fd52de3c23511303ddf772fe2b740154c6d45afa6284dd84,&sov=1,&tt=1575208986074,&did=10000000000000000000000000001501

注:斗鱼曾经升级过一次前端加密技术,用 WebAssembly 来加密,这里的参数并不是实际的请求参数。直接搜索并不能找到生成的地方,调试起来也很复杂,以下跳过博主还没弄明白的地方。

在调试过程中先看到 did 的获取方式,这里显示的更简单:return 如果从 cookie 中获取不到就返回 "10000000000000000000000000001501"。接着直接调用了 ub98484234,传入参数未变,即 rid,did,时间戳,返回也和移动版一样有 sign 值,但在上一步 POST 的参数里并没有看到 sign,果然又是处理过的,继续调试找到 window.SQSSMURFNEEDALONGLONGLONGNAME.sqsneedalongname (e, i, a, n, o, 3186),此处的运算过程较为复杂,暂不深究,但既然是客户端用来加密 POST 参数的,我们来试试不进行第二次加密直接 POST 试试。

RxACJ.prototype.getDid = function getDid() {
return this.cookie.get("did") || "10000000000000000000000000001501"
}
//省略部分
function loadStream(e, t, r, i, a, n) {
			//省略部分
            var l = Object(h.j)(["app", "rid"])
              , f = Object(ee.d)()
              , p = parseInt((new Date).getTime() / 1e3, 10);
            void 0 === e && (e = Object(h.j)(["app", "cdn"])),
            void 0 === t && (t = Object(h.j)(["app", "rate"]));
            var _ = void 0
              , m = Object(h.j)(["videoModule", "haveShowVideo"]) ? 0 : 1
              , y = 0;
            g.b(g.a.RateRecordTime, 0) >= 3 && (y = 1);
            try {
                _ = window.ub98484234(l, f, p) + "&cdn=" + e + "&rate=" + t + "&ver=" + S.g + "&iar=" + m + "&ive=" + y
            } catch (e) {
            //省略部分 
			var u = Object(ee.d)(), d = "cdn=" + n + "&rate=" + s + "&ver=" + S.g + "&iar=" + i + "&ive=" + a + "&" + e + "&sov=1&tt=" + t + "&did=" + u;
return Promise.resolve().then(function () {
    		return o.httpClient.post(String, (Object(c.isSupermanager)() || Object(c.getIsHostlive)() ? S.b : S.a) + "/" + r, d).subscribe(function (e) {
//省略部分               
function onRuntimeInitialized() {
            var e = +Object(h.j)(["app", "rid"])
              , t = (new Date).getTime()
              , r = t.toString(16)
              , i = parseInt("0x" + r.substring(0, r.length - 8), 16)
              , a = parseInt("0x" + r.substring(r.length - 8, r.length), 16)
              , n = Object(ee.d)()
              , o = Object(ee.c)("auth") || "201806211659380f83e682207dd1293cea68f98662b1660083967948219fe20";
            Promise.resolve().then(function() {
                return ue = (new Date).getTime(),
                window.SQSSMURFNEEDALONGLONGLONGNAME.sqsneedalongname(e, i, a, n, o, 3186)

找到在二次加密前的 POST 参数,即 v=220120191201、&did=15d429facdc5bc0ae027594400021501,&tt=1575213485,&sign=ecaf7d9292b4b658a52002a9ad52a065,&cdn=cmcc,&rate=2,&ver=Douyu_219112905,&iar=0,&ive=1,和移动网页端的差不多,主要是 did、sign 和 tt 的变化,POST 地址 https://www.douyu.com/lapi/live/getH5Play/ 加 rid,测试能成功返回我们要的播放地址。

{
    "error": 0,
    "msg": "ok",
    "data": {
        "room_id": 85894,
        "is_mixed": false,
        "mixed_live": "",
        "mixed_url": "",
        "rtmp_cdn": "cmcc",
        "rtmp_url": "https://play1.douyuscdn.com/live",
        "rtmp_live": "85894rmovieChow_550p.flv?wsAuth=8dafde92bad56de26956e0120d40eaa3&token=web-h5-0-85894-a3575d938a4aa309a0707b077232e93e6dfedd331bafa44b&logo=0&expire=0&did=15d429facdc5bc0ae027594400021501&ver=Douyu_219112905&pt=2&st=0&vhost=play1&origin=all&mix=0&isp=cmcc",

Python 代码实现

预览获取斗鱼直播源

Python 模拟 PC 网页版的预览获取斗鱼直播源,注意:不是所有直播间都有预览权限,代码如下:

import requests
import re
import time
import hashlib

def get_url(rid):
    # 匹配真实房间号
    room_url = 'https://m.douyu.com/' + rid
    response = requests.get(url=room_url)
    rid = re.findall(r'"rid":(\d{1,7})', response.text)[0]
    # POST
    request_url = 'https://playweb.douyucdn.cn/lapi/live/hlsH5Preview/' + rid
    post_data = {
        'rid': rid,
        'did': '10000000000000000000000000001501'
    }
    tt = str(int(time.time()))
    auth = hashlib.md5((rid + tt).encode('utf-8')).hexdigest() 
    header = {
        'content-type': 'application/x-www-form-urlencoded',
        'rid': rid,
        'time': tt,
        'auth': auth
    }
    response = requests.post(url=request_url, headers=header, data=post_data).json()
    pre_url = ''
    if response.get('error') == 0:
        rtmp_live = response.get('data').get('rtmp_live')
        pre_url = re.findall(r'({}[0-9a-zA-Z]+)_'.format(rid), rtmp_live)[0]
        real_url = 'http://tx2play1.douyucdn.cn/live/{}.flv?uuid='.format(pre_url)
    else:
        real_url = '不支持或未开播'
    return real_url

播放页获取斗鱼直播源

Python 模拟手机网页版播放页获取斗鱼直播源,实现有两种思路:

一是不安装 crypto-js 模块,修改 ub98484234 函数使其返回 strc,再修改 strc 中的 var rb = CryptoJS.MD5 (cb).toString (); 使用 Python 的 hashlib 模块计算 MD5。

二是安装 Nodejs 后,npm 安装 crypto-js 模块,在 ub98484234 函数前引入,最后 Python 中使用 PyExecJS 模块执行 js 代码,即可返回这种形式的字符串:“v=250120200510&did=4b98044a3696d65bd4f6693300011531&tt=1589122520&sign=a76fb336fcff56bd54b54ebc06302712”,这样比较简洁。代码如下:

# 移动网页端播放页获取
# 先在Nodejs中安装crypto-js模块

import requests
import re
import execjs
import time

def get_room_url(rid):
    # 匹配真实房间号
    room_url = 'https://m.douyu.com/' + rid
    response = requests.get(url=room_url)
    rid = re.findall(r'"rid":(\d{1,7})', response.text)[0]
    # 匹配源码中ub98484234函数
    pattern = r'(function ub9[\s\S]*?)function'
    result = re.findall(pattern, response.text)[0]
    # 引入crypto-js模块
    ub98484234 = 'var CryptoJS = require("crypto-js");' + result
    tt = str(int(time.time()))
    data = execjs.compile(ub98484234).call(
        'ub98484234', rid, '10000000000000000000000000001501', tt)
    sign = re.findall(r'sign=(.{32})', data)[0]
    post_data = {
        'v': '2501' + time.strftime('%Y%m%d', time.localtime()),
        'did': '10000000000000000000000000001501',
        'tt': tt,
        'sign': sign,
        'ver': '219032101',
        'rid': rid,
        'rate': '-1'
    }
    response = requests.post('https://m.douyu.com/api/room/ratestream', data=post_data).json()
    real_url = response.get('data').get('url')
    return real_url

其他

1、注意 VIP 房间号

部分直播有 VIP 房间号,如 520、980、9999 这样的短号并非真实房间号,可以先从直播间源码中获取对应的 room_id 后,再进行下一步处理,否则无法获取到真实地址。

2、其他各种直播平台的获取方法可查看博主的 GitHub 仓库:real-url

3、参考资料:吾爱破解:斗鱼直播真实地址解析,直播源抓取方法

» 链接地址:https://wbt5.com/douyu-live.html »英雄不问来路,转载请注明出处。

详细记录斗鱼直播源调试过程》上有 3 条评论

  1. noecs

    eval中的函数,其中`var k2`的值,跟着页面不断变化。这个怎么处理,最初的`ub98484234`函数也调用了外部一个16进制的数组,此数组也一直在变化,此变量名也同步在变,这就很恶心了。

    回复
      1. noecs

        代码块也不太好截取,eval中的函数还引入了第三方加密包。感觉困难重重。我用的golang

        回复

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注