Skip to content

Instantly share code, notes, and snippets.

@jamesliu96
Last active January 2, 2023 17:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesliu96/156e8bf8722581122e0ce104d4b096d0 to your computer and use it in GitHub Desktop.
Save jamesliu96/156e8bf8722581122e0ce104d4b096d0 to your computer and use it in GitHub Desktop.
Bilibili Live Danmaku Client for Node.js
function exitWithErr(...args) {
console.error('error:', ...args);
process.exit(-1);
}
let roomId = process.argv[2];
if (!roomId) exitWithErr('please provide roomId');
const DEBUG = false;
const { inflateSync } = require('zlib');
const colors = require('colors');
const { default: axios } = require('axios');
const WebSocket = require('ws');
const cheerio = require('cheerio');
/** @returns {Promise<{token:string,host_list:{host:string}[]}>} */
async function getDanmuInfo() {
const { data: html = '' } = await axios.get(
`https://live.bilibili.com/${roomId}`,
{
headers: {
host: 'live.bilibili.com',
accept: '*/*',
},
}
);
const pad = 'window.__NEPTUNE_IS_MY_WAIFU__=';
const $ = cheerio.load(html);
roomId = `${
JSON.parse(
$('script')
.filter(function () {
return $(this).html().startsWith(pad);
})
.html()
.slice(pad.length)
).roomInfoRes.data.room_info.room_id
}`;
const { data = {} } = await axios.get(
`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${roomId}`
);
return data['code'] === 0 ? data['data'] : {};
}
const logger = {
log(t = '', ...args) {
if (DEBUG)
console.log(
colors.cyan(`> ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`)
);
},
warn(t = '', ...args) {
if (DEBUG)
console.warn(
colors.yellow(`! ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`)
);
},
error(t = '', ...args) {
if (DEBUG)
console.error(
colors.red(`* ${t} ${args.map((a) => JSON.stringify(a)).join('\t')}`)
);
},
};
const heartbeat = Buffer.from([
0x00, 0x00, 0x00, 0x1f, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00,
]);
const colorList = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'].reduce(
(acc, cur) => {
const [f, ...r] = cur;
return [...acc, cur, `bright${f.toUpperCase()}${r.join('')}`];
},
[]
);
const colorMap = new Map();
function handleChunk(chunk = {}) {
const { cmd, data, info } = chunk;
if (cmd === 'DANMU_MSG') {
const user = info[2];
const badge = info[3];
const userId = user[0];
let color;
if (colorMap.has(userId)) {
color = colorMap.get(userId);
} else {
color = colorList[~~(Math.random() * colorList.length)];
colorMap.set(userId, color);
}
console.log(
colors.bold[color](
`${badge[0] ? `[${badge[1]}(${badge[0]})] ` : ''}${user[1]}: ${info[1]}`
)
);
} else {
logger.log(cmd, data);
}
}
/** @param {Buffer} msg */
function handleMessage(msg) {
const packetLen = msg.readUInt32BE(0);
msg = msg.slice(0, packetLen);
const headerLen = msg.readUInt16BE(4);
const _h = msg.slice(0, headerLen);
let body = msg.slice(headerLen);
const ver = _h.readUInt16BE(6);
const op = _h.readUInt32BE(8);
if (ver === 2) {
body = inflateSync(body);
const b = [];
for (let offset = 0; offset + 1 < body.length; ) {
const pLen = body.readUInt32BE(offset);
const item = body.slice(offset, offset + pLen);
b.push(item.slice(item.readUInt16BE(4)));
offset += pLen;
}
body = b;
}
if (op === 5) {
if (Array.isArray(body)) {
body.forEach((b) => {
try {
handleChunk(JSON.parse(b.toString()));
} catch (e) {}
});
} else {
try {
handleChunk(JSON.parse(body.toString()));
} catch (e) {}
}
}
}
(async () => {
let _;
try {
_ = await getDanmuInfo();
} catch (e) {
return exitWithErr('api fetch - network failed', e);
}
const { token = '', host_list: hostList = [] } = _;
if (!hostList.length) return exitWithErr('api fetch - ws host list empty');
const [{ host }] = hostList;
const ws = new WebSocket(`wss://${host}/sub`);
ws.on('open', () => {
logger.warn('open');
const d = JSON.stringify({
uid: 0,
roomid: +roomId,
protover: 2,
platform: 'web',
type: 2,
key: token,
});
const h = Buffer.from([
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07,
0x00, 0x00, 0x00, 0x00,
]);
h.writeUInt32BE(d.length + h.length, 0);
ws.send(Buffer.from([...h, ...new TextEncoder().encode(d)]));
setInterval(() => {
ws.send(heartbeat);
}, 30000);
});
ws.on('error', (err) => {
logger.error('error', err);
});
ws.on('close', (code, reason) => {
logger.warn('close', code, reason);
});
ws.on('message', (msg) => {
logger.log('message');
handleMessage(msg);
});
})();
{
"name": "dc",
"version": "1.0.0",
"main": "dc.js",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"cheerio": "^1.0.0-rc.10",
"colors": "^1.4.0",
"ws": "^7.5.3"
},
"devDependencies": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment