const WebSocket = require("ws");
const _ = require("lodash");
const { BrotliDecode } = require("./brotli");
const { WS_CONSTANTS, WS_BINARY_HEADER_LIST } = require("./constants");
const { getRoomIdAndToken } = require("./server");
function toUint8Array(bufferObject) {
var arrayBuffer = new ArrayBuffer(bufferObject.length);
var typedArray = new Uint8Array(arrayBuffer);
for (var i = 0; i < bufferObject.length; ++i) {
typedArray[i] = bufferObject[i];
}
return typedArray;
}
function connectDanmu(roomId, token) {
const ws = new WebSocket("wss://tx-gz-live-comet-02.chat.bilibili.com/sub", {
origin: "https://live.bilibili.com",
});
ws.on("message", (payload) => {
console.log(payload);
const data = decodeData(payload);
console.log(data);
console.log(data.body);
});
ws.on("open", () => {
ws.send(
packageObject(7, {
uid: 0,
roomid: Number(roomId),
protover: 3,
platform: "web",
type: 2,
key: token,
})
);
// heartbeat packets
console.log("heartbeat");
ws.send(packageHeartbeat());
const heartbeat = setInterval(() => {
console.log("heartbeat");
ws.send(packageHeartbeat());
}, 30 * 1000);
});
ws.on("close", (code, reason) => {
console.log("close", code, reason);
});
ws.on("error", (err) => {
console.error(err);
});
}
function packageHeartbeat() {
const body = new TextEncoder().encode({});
return packageBinary(2, body);
}
function packageBinary(type, body) {
// console.log("packageBinary", type, body);
const tmp = new Uint8Array(16 + body.byteLength);
const headDataView = new DataView(tmp.buffer);
headDataView.setInt32(0, tmp.byteLength);
headDataView.setInt16(4, 16);
headDataView.setInt16(6, 1);
headDataView.setInt32(8, type); // verify
headDataView.setInt32(12, 1);
tmp.set(body, 16);
// console.log(tmp);
return tmp;
}
function packageObject(type, bufferObj) {
// console.log("packageObject", type, bufferObj);
return packageBinary(
type,
new TextEncoder().encode(JSON.stringify(bufferObj))
);
}
function decodeData(buffer) {
const arr = toUint8Array(buffer);
const dataView = new DataView(arr.buffer);
const result = {
body: [],
};
result.packetLen = dataView.getInt32(WS_CONSTANTS.WS_PACKAGE_OFFSET);
WS_BINARY_HEADER_LIST.forEach((header) => {
if (header.bytes === 4) {
result[header.key] = dataView.getInt32(header.offset);
}
if (header.bytes === 2) {
result[header.key] = dataView.getInt16(header.offset);
}
});
console.log(result);
if (
!result.op ||
(WS_CONSTANTS.WS_OP_MESSAGE !== result.op &&
result.op !== WS_CONSTANTS.WS_OP_CONNECT_SUCCESS)
) {
result.op &&
WS_CONSTANTS.WS_OP_HEARTBEAT_REPLY === result.op &&
(result.body = {
count: dataView.getInt32(WS_CONSTANTS.WS_PACKAGE_HEADER_TOTAL_LENGTH),
});
} else {
console.log("parsing non heartbeats");
for (
let cursor = WS_CONSTANTS.WS_PACKAGE_OFFSET,
end = result.packetLen,
start = "",
payload = "";
cursor < buffer.byteLength;
cursor += end
) {
(end = dataView.getInt32(cursor)),
(start = dataView.getInt16(cursor + WS_CONSTANTS.WS_HEADER_OFFSET));
try {
if (result.ver === WS_CONSTANTS.WS_BODY_PROTOCOL_VERSION_NORMAL) {
console.log(cursor, start, end);
var normalDecoded = new TextDecoder().decode(
buffer.slice(cursor + start, cursor + end)
);
payload =
0 !== normalDecoded.length ? JSON.parse(normalDecoded) : null;
} else if (
result.ver === WS_CONSTANTS.WS_BODY_PROTOCOL_VERSION_BROTLI
) {
var slice = buffer.slice(cursor + start, cursor + end),
brotliDecoded = BrotliDecode(toUint8Array(slice));
result.body = decodeData(Buffer.from(brotliDecoded)).body;
}
payload && result.body.push(payload);
} catch (err) {
console.error(
"decode body error:",
new Uint8Array(buffer),
result,
err
);
}
}
}
return result;
}
getRoomIdAndToken(process.env.ROOM_ID || "1367262").then(
({ roomId, token }) => {
console.log(roomId, token);
connectDanmu(roomId, token);
}
);