UNPKG

douyin-danma-listener

Version:
355 lines (354 loc) 11.8 kB
import { parse, format } from "node:url"; import WebSocket from "ws"; import { TypedEmitter } from "tiny-typed-emitter"; import { decompressGzip, getXMsStub, getSignature, getUserUniqueId } from "./utils.js"; import protobuf from "./proto.js"; import { getCookie } from "./api.js"; function buildRequestUrl(url) { const parsedUrl = parse(url, true); const existingParams = parsedUrl.query; existingParams["aid"] = "6383"; existingParams["device_platform"] = "web"; existingParams["browser_language"] = "zh-CN"; existingParams["browser_platform"] = "Win32"; existingParams["browser_name"] = "Mozilla"; existingParams["browser_version"] = "92.0.4515.159"; parsedUrl.search = null; parsedUrl.query = existingParams; return format(parsedUrl); } class DouYinDanmaClient extends TypedEmitter { ws; roomId; heartbeatInterval; heartbeatTimer; isHeartbeatRunning = false; autoStart; autoReconnect; reconnectAttempts; cookie; timeoutInterval; lastMessageTime; timeoutTimer; isTimeoutCheckRunning = false; constructor(roomId, options = {}) { super(); this.roomId = roomId; this.heartbeatInterval = options.heartbeatInterval ?? 10000; this.autoStart = options.autoStart ?? false; this.autoReconnect = options.autoReconnect ?? 10; this.reconnectAttempts = 0; this.cookie = options.cookie; this.timeoutInterval = options.timeoutInterval ?? 100000; // 默认100秒 this.lastMessageTime = Date.now(); if (this.autoStart) { this.connect(); } } async connect() { const url = await this.getWsInfo(this.roomId); if (!url) { this.emit("error", new Error("获取抖音弹幕签名失败")); return; } const cookies = this.cookie || (await getCookie()); this.ws = new WebSocket(url, { headers: { Cookie: cookies, }, }); this.ws.on("open", () => { this.emit("open"); this.startHeartbeat(); this.startTimeoutCheck(); }); this.ws.on("message", (data) => { this.lastMessageTime = Date.now(); this.decode(data); }); this.ws.on("close", () => { this.emit("close"); this.reconnect(); }); this.ws.on("error", (error) => { this.emit("error", error); this.reconnect(); }); } send(data) { if (!this.ws) { return; } this.ws.send(data); } close() { if (!this.ws) { return; } this.reconnectAttempts = this.autoReconnect; this.stopHeartbeat(); this.stopTimeoutCheck(); this.ws.close(); } startHeartbeat() { if (this.isHeartbeatRunning) { return; } this.stopHeartbeat(); this.isHeartbeatRunning = true; this.heartbeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.emit("heartbeat"); this.send(":\x02hb"); } else { console.log("连接未就绪,当前状态:", this.ws.readyState); } }, this.heartbeatInterval); } stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.isHeartbeatRunning = false; } } startTimeoutCheck() { if (this.isTimeoutCheckRunning) { return; } this.stopTimeoutCheck(); this.isTimeoutCheckRunning = true; // 重置最后消息时间,给连接一些初始化时间 this.lastMessageTime = Date.now(); this.timeoutTimer = setInterval(() => { const now = Date.now(); if (now - this.lastMessageTime > this.timeoutInterval) { console.log("No message received for too long, reconnecting..."); // 在重连前重置时间,避免立即触发下一次重连 this.lastMessageTime = now; this.reconnect(); } }, 1000); } stopTimeoutCheck() { if (this.timeoutTimer) { clearInterval(this.timeoutTimer); this.isTimeoutCheckRunning = false; } } reconnect() { this.stopHeartbeat(); this.stopTimeoutCheck(); if (this.reconnectAttempts < this.autoReconnect) { this.reconnectAttempts++; this.connect(); this.emit("reconnect", this.reconnectAttempts); } } async handleMessage() { } /** * 处理弹幕消息 */ async handleChatMessage(chatMessage) { this.emit("chat", chatMessage); this.emit("message", chatMessage); } /** * 处理进入房间 */ async handleEnterRoomMessage(message) { this.emit("member", message); this.emit("message", message); } /** * 处理礼物消息 */ async handleGiftMessage(message) { this.emit("gift", message); this.emit("message", message); } /** * 处理点赞消息 */ async handleLikeMessage(message) { this.emit("like", message); this.emit("message", message); } /** * 处理social消息 */ async handleSocialMessage(message) { this.emit("social", message); this.emit("message", message); } /** * 处理RoomUserSeqMessage */ async handleRoomUserSeqMessage(message) { this.emit("roomUserSeq", message); this.emit("message", message); } /** * 处理 WebcastRoomStatsMessage */ async handleRoomStatsMessage(message) { this.emit("roomStats", message); this.emit("message", message); } /** * 处理 WebcastRoomRankMessage */ async handleRoomRankMessage(message) { this.emit("roomRank", message); this.emit("message", message); } /** * 处理其他消息 */ async handleOtherMessage(message) { this.emit("message", message); } async decode(data) { // @ts-ignore const PushFrame = protobuf.douyin.PushFrame; // @ts-ignore const Response = protobuf.douyin.Response; // @ts-ignore const ChatMessage = protobuf.douyin.ChatMessage; // @ts-ignore const RoomUserSeqMessage = protobuf.douyin.RoomUserSeqMessage; // @ts-ignore const MemberMessage = protobuf.douyin.MemberMessage; // @ts-ignore const GiftMessage = protobuf.douyin.GiftMessage; // @ts-ignore const LikeMessage = protobuf.douyin.LikeMessage; // @ts-ignore const SocialMessage = protobuf.douyin.SocialMessage; // @ts-ignore const RoomStatsMessage = protobuf.douyin.RoomStatsMessage; // @ts-ignore const RoomRankMessage = protobuf.douyin.RoomRankMessage; const wssPackage = PushFrame.decode(data); // @ts-ignore const logId = wssPackage.logId; let decompressed; try { // @ts-ignore if (wssPackage.payload instanceof Buffer) { // @ts-ignore decompressed = await decompressGzip(wssPackage.payload); } else { return; } } catch (e) { this.emit("error", e); return; } const payloadPackage = Response.decode(decompressed); let ack = null; // @ts-ignore if (payloadPackage.needAck) { const obj = PushFrame.create({ logId: logId, // @ts-ignore payloadType: payloadPackage.internalExt, }); ack = PushFrame.encode(obj).finish(); } const msgs = []; // @ts-ignore for (const msg of payloadPackage.messagesList) { // const now = new Date(); try { if (msg.method === "WebcastChatMessage") { const chatMessage = ChatMessage.decode(msg.payload); this.handleChatMessage(chatMessage.toJSON()); } else if (msg.method === "WebcastMemberMessage") { const memberMessage = MemberMessage.decode(msg.payload); this.handleEnterRoomMessage(memberMessage.toJSON()); } else if (msg.method === "WebcastGiftMessage") { const giftMessage = GiftMessage.decode(msg.payload); this.handleGiftMessage(giftMessage.toJSON()); } else if (msg.method === "WebcastLikeMessage") { const message = LikeMessage.decode(msg.payload); this.handleLikeMessage(message.toJSON()); } else if (msg.method === "WebcastSocialMessage") { const message = SocialMessage.decode(msg.payload); this.handleSocialMessage(message.toJSON()); } else if (msg.method === "WebcastRoomUserSeqMessage") { const message = RoomUserSeqMessage.decode(msg.payload); this.handleRoomUserSeqMessage(message.toJSON()); } else if (msg.method === "WebcastRoomStatsMessage") { const message = RoomStatsMessage.decode(msg.payload); this.handleRoomStatsMessage(message.toJSON()); } else if (msg.method === "WebcastRoomRankMessage") { const message = RoomRankMessage.decode(msg.payload); this.handleRoomRankMessage(message.toJSON()); } else { // WebcastRanklistHourEntranceMessage,WebcastInRoomBannerMessage,WebcastRoomStreamAdaptationMessage } } catch (e) { console.error("error:", e, msg); } } if (ack) { this.send(ack); } return [msgs, ack]; } async getWsInfo(roomId) { const userUniqueId = getUserUniqueId(); // const userUniqueId = "7877922945687137703"; const versionCode = 180800; const webcastSdkVersion = "1.0.14-beta.0"; const sigParams = { live_id: "1", aid: "6383", version_code: versionCode, webcast_sdk_version: webcastSdkVersion, room_id: roomId, sub_room_id: "", sub_channel_id: "", did_rule: "3", user_unique_id: userUniqueId, device_platform: "web", device_type: "", ac: "", identity: "audience", }; let signature; try { const m = getXMsStub(sigParams); signature = getSignature(m); // 这里应该获取签名 } catch (e) { return; } const webcast5Params = { room_id: roomId, compress: "gzip", version_code: String(versionCode), webcast_sdk_version: webcastSdkVersion, live_id: "1", did_rule: "3", user_unique_id: userUniqueId, identity: "audience", signature: signature.toString(), }; const wssUrl = `wss://webcast5-ws-web-lf.douyin.com/webcast/im/push/v2/?${new URLSearchParams(webcast5Params).toString()}`; return buildRequestUrl(wssUrl); } } export default DouYinDanmaClient;