UNPKG

douyin-danma-listener

Version:
399 lines (398 loc) 13.8 kB
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"; class DouYinDanmaClient extends TypedEmitter { ws; roomId; heartbeatInterval; heartbeatTimer; isHeartbeatRunning = false; autoStart; autoReconnect; reconnectAttempts; reconnectInterval; cookie; timeoutInterval; lastMessageTime; timeoutTimer; isTimeoutCheckRunning = false; isReconnecting = false; host; 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.reconnectInterval = options.reconnectInterval ?? 10000; this.cookie = options.cookie; this.timeoutInterval = options.timeoutInterval ?? 100000; // 默认100秒 this.lastMessageTime = Date.now(); this.host = options.host ?? "webcast100-ws-web-hl.douyin.com"; if (this.autoStart) { this.connect(); } } async connect() { const url = await this.getWsInfo(this.roomId); if (!url) { this.emit("error", new Error("获取抖音弹幕签名失败")); return; } this.emit("init", url); const cookies = this.cookie || (await getCookie()); this.ws = new WebSocket(url, { headers: { Cookie: cookies, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", Origin: "https://live.douyin.com", Referer: "https://live.douyin.com/", }, }); this.ws.on("open", () => { this.reconnectAttempts = 0; 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(); if (this.ws.readyState === WebSocket.OPEN) { 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() { if (this.isReconnecting) { return; } this.stopHeartbeat(); this.stopTimeoutCheck(); if (this.reconnectAttempts < this.autoReconnect) { this.isReconnecting = true; this.reconnectAttempts++; setTimeout(() => { this.connect(); this.isReconnecting = false; this.emit("reconnect", this.reconnectAttempts); }, this.reconnectInterval); } } 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 handlePrivilegeScreenChatMessage(message) { this.emit("privilegeScreenChat", message); this.emit("message", message); } async handleScreenChatMessage(message) { this.emit("screenChat", 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; // @ts-ignore const PrivilegeScreenChatMessage = protobuf.douyin.PrivilegeScreenChatMessage; // @ts-ignore const ScreenChatMessage = protobuf.douyin.ScreenChatMessage; 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 if (msg.method === "WebcastPrivilegeScreenChatMessage") { const message = PrivilegeScreenChatMessage.decode(msg.payload); this.handlePrivilegeScreenChatMessage(message.toJSON()); } else if (msg.method === "WebcastScreenChatMessage") { const message = ScreenChatMessage.decode(msg.payload); this.handleScreenChatMessage(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.15"; 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 = { app_name: "douyin_web", room_id: roomId, compress: "gzip", version_code: String(versionCode), webcast_sdk_version: webcastSdkVersion, update_version_code: webcastSdkVersion, live_id: "1", did_rule: "3", user_unique_id: userUniqueId, identity: "audience", signature: signature.toString(), device_platform: "web", cookie_enabled: "true", screen_width: "1920", screen_height: "1080", browser_language: "zh-CN", browser_platform: "Win32", browser_name: "Mozilla", browser_version: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", browser_online: "true", tz_name: "Etc/GMT-8", host: "https://live.douyin.com", aid: "6383", endpoint: "live_pc", support_wrds: "1", im_path: "/webcast/im/fetch/", need_persist_msg_count: "15", heartbeatDuration: "0", }; const wssUrl = `wss://${this.host}/webcast/im/push/v2/?${new URLSearchParams(webcast5Params).toString()}`; return wssUrl; } } export default DouYinDanmaClient;