3Shain / Comen

📺直播用弹幕栏【原bilichat】

Home Page:https://comen.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

b站协议升级

lhr0909 opened this issue · comments

前几天还能用,今天发现protover升级到3,原来的协议其实还能连接和heartbeat,但是出了一个新的ver 3的封包,用的是BrotliDecode。我本来想直接用Comen的包来做弹幕解析的,但是发现了这个问题,所以搬了一下b站的最新代码,并且使用brotliDecode算法可以正常解包。

附上我的部分代码:

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);
  }
);

我刚实现并测试了新协议,没有什么问题。但旧协议同样也没有什么问题(当客户端第一个package发送protover等于2时,接收的包也应当只有2,目前没有发现特例。如果是混杂了不同协议的包,目前的策略也仅为忽略而不会造成不可恢复的异常)。暂时决定维持现状。

@3Shain 好的 如果旧协议没影响就好 因为我最近测试的时候发现线上的版本接收不了弹幕了 浏览器看封包的话的确发现有压缩过的封包 所以我开这个issue 另外也感谢作者及时添加新协议解析(最近在做b站视频 估计会提到您的repo 哈哈哈)

Cheers!