mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
feat: add heartbeat monitoring system and disable browser timer throttling
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { KeepLiveWS } from 'bilibili-live-ws/browser' // 导入 bilibili-live-ws 库
|
||||
import { LiveWS } from "bilibili-live-danmaku";
|
||||
// BaseDanmakuClient.ts
|
||||
import type { EventModel } from '@/api/api-models'
|
||||
// 导入事件模型和类型枚举
|
||||
@@ -13,7 +13,7 @@ export default abstract class BaseDanmakuClient {
|
||||
}
|
||||
|
||||
// WebSocket 客户端实例
|
||||
public client: KeepLiveWS | null
|
||||
public client: LiveWS | null
|
||||
|
||||
// 客户端连接状态
|
||||
public state: 'padding' | 'connected' | 'connecting' | 'disconnected'
|
||||
@@ -35,6 +35,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: ((arg1: EventModel, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 关注事件
|
||||
like: ((arg1: EventModel, arg2?: any) => void)[] // 新增: 点赞事件
|
||||
}
|
||||
|
||||
// --- 事件系统 2: 使用原始数据类型 ---
|
||||
@@ -48,6 +49,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: ((arg1: any, arg2?: any) => void)[] // 新增: SC 删除事件
|
||||
all: ((arg1: any) => void)[] // 'all' 事件监听器接收原始消息或特定事件包
|
||||
follow: ((arg1: any, arg2?: any) => void)[] // 新增: 关注事件
|
||||
like: ((arg1: any, arg2?: any) => void)[] // 新增: 点赞事件
|
||||
}
|
||||
|
||||
// 创建空的 EventModel 监听器对象
|
||||
@@ -61,6 +63,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
like: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +78,7 @@ export default abstract class BaseDanmakuClient {
|
||||
scDel: [],
|
||||
all: [],
|
||||
follow: [], // 初始化 follow 事件
|
||||
like: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,27 +182,27 @@ export default abstract class BaseDanmakuClient {
|
||||
* @returns Promise<{ success: boolean; message: string }> 连接结果
|
||||
*/
|
||||
protected async initClientInner(
|
||||
chatClient: KeepLiveWS,
|
||||
chatClient: LiveWS,
|
||||
): Promise<{ success: boolean, message: string }> {
|
||||
let isConnected = false // 标记是否连接成功
|
||||
let isError = false // 标记是否发生错误
|
||||
let errorMsg = '' // 存储错误信息
|
||||
|
||||
// 监听错误事件
|
||||
chatClient.on('error', (err: any) => {
|
||||
chatClient.addEventListener('error', (err: any) => {
|
||||
console.error(`[${this.type}] 客户端发生错误:`, err)
|
||||
isError = true
|
||||
errorMsg = err?.message || err?.toString() || '未知错误'
|
||||
})
|
||||
|
||||
// 监听连接成功事件
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接成功`)
|
||||
isConnected = true
|
||||
})
|
||||
|
||||
// 监听连接关闭事件
|
||||
chatClient.on('close', () => {
|
||||
chatClient.addEventListener('close', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端连接已关闭`)
|
||||
if (this.state !== 'disconnected') {
|
||||
this.state = 'disconnected'
|
||||
@@ -209,7 +213,7 @@ export default abstract class BaseDanmakuClient {
|
||||
|
||||
// 监听原始消息事件 (通用)
|
||||
// 注意: 子类可能也会监听特定事件名, 这里的 'msg' 是备用或处理未被特定监听器捕获的事件
|
||||
chatClient.on('msg', (command: any) => this.onRawMessage(command))
|
||||
chatClient.addEventListener('MESSAGE', (command: any) => this.onRawMessage(command.data))
|
||||
|
||||
this.client = chatClient // 保存客户端实例
|
||||
|
||||
@@ -301,6 +305,12 @@ export default abstract class BaseDanmakuClient {
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onScDel(comand: any): void
|
||||
/**
|
||||
* 处理点赞消息 (子类实现)
|
||||
* @param data - 原始消息数据部分 (any 类型)
|
||||
* @param rawCommand - 完整的原始消息对象 (可选, any 类型)
|
||||
*/
|
||||
public abstract onLike(comand: any): void
|
||||
|
||||
// --- 事件系统 1: on/off (使用 EventModel) ---
|
||||
public onEvent(eventName: 'danmaku', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
@@ -311,6 +321,7 @@ export default abstract class BaseDanmakuClient {
|
||||
public onEvent(eventName: 'scDel', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(eventName: 'follow', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: 'like', listener: (arg1: EventModel, arg2?: any) => void): this // 新增
|
||||
public onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
// @ts-ignore
|
||||
@@ -342,6 +353,7 @@ export default abstract class BaseDanmakuClient {
|
||||
public on(eventName: 'scDel', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public on(eventName: 'follow', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: 'like', listener: (arg1: any, arg2?: any) => void): this // 新增
|
||||
public on(eventName: keyof BaseDanmakuClient['eventsRaw'], listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsRaw[eventName]) {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { DataEvent, LiveWS, MessageData } from 'bilibili-live-danmaku'
|
||||
import { EventDataTypes, GuardLevel } from '@/api/api-models'
|
||||
import { GuidUtils } from '@/Utils'
|
||||
import { AVATAR_URL } from '../constants'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
import Long from 'long'
|
||||
|
||||
export interface DirectClientAuthInfo {
|
||||
token: string
|
||||
@@ -28,22 +29,34 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
if (this.authInfo) {
|
||||
const chatClient = new KeepLiveWS(this.authInfo.roomId, {
|
||||
const chatClient = new LiveWS(this.authInfo.roomId, {
|
||||
key: this.authInfo.token,
|
||||
buvid: this.authInfo.buvid,
|
||||
uid: this.authInfo.tokenUserId,
|
||||
protover: 3,
|
||||
})
|
||||
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(`[direct] 已连接房间: ${this.authInfo.roomId}`)
|
||||
})
|
||||
chatClient.on('DANMU_MSG', data => this.onDanmaku(data))
|
||||
chatClient.on('SEND_GIFT', data => this.onGift(data))
|
||||
chatClient.on('GUARD_BUY', data => this.onGuard(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE', data => this.onSC(data))
|
||||
chatClient.on('INTERACT_WORD', data => this.onEnter(data))
|
||||
chatClient.on('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
|
||||
chatClient.addEventListener('DANMU_MSG', data => this.onDanmaku(data.data))
|
||||
chatClient.addEventListener('SEND_GIFT', data => this.onGift(data.data))
|
||||
chatClient.addEventListener('GUARD_BUY', data => this.onGuard(data.data))
|
||||
chatClient.addEventListener('SUPER_CHAT_MESSAGE', data => this.onSC(data.data))
|
||||
//chatClient.addEventListener('INTERACT_WORD', data => this.onEnter(data.data))
|
||||
chatClient.addEventListener('MESSAGE', data => {
|
||||
switch (data.data.cmd) {
|
||||
case 'INTERACT_WORD_V2':
|
||||
this.onEnter(data.data)
|
||||
break
|
||||
case 'LIKE_INFO_V3_CLICK':
|
||||
this.onLike(data.data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
//chatClient.addEventListener('SUPER_CHAT_MESSAGE_DELETE', data => this.onScDel(data))
|
||||
|
||||
return super.initClientInner(chatClient)
|
||||
} else {
|
||||
@@ -55,7 +68,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
}
|
||||
}
|
||||
|
||||
public onDanmaku(command: any): void {
|
||||
public onDanmaku(command: MessageData.DANMU_MSG): void {
|
||||
const info = command.info
|
||||
this.eventsRaw?.danmaku?.forEach((d) => {
|
||||
d(info, command)
|
||||
@@ -84,7 +97,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onGift(command: any): void {
|
||||
public onGift(command: MessageData.SEND_GIFT): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.gift?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -96,13 +109,13 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.giftName,
|
||||
price: data.price / 1000,
|
||||
price: data.total_coin / 1000,
|
||||
num: data.num,
|
||||
time: Date.now(),
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
fans_medal_level: data.fans_medal?.medal_level,
|
||||
fans_medal_name: data.fans_medal?.medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal !== null || data.fans_medal !== undefined,
|
||||
uface: data.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
@@ -112,7 +125,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onSC(command: any): void {
|
||||
public onSC(command: MessageData.SUPER_CHAT_MESSAGE): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.sc?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -130,7 +143,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
guard_level: data.user_info.guard_level,
|
||||
fans_medal_level: data.medal_info.medal_level,
|
||||
fans_medal_name: data.medal_info.medal_name,
|
||||
fans_medal_wearing_status: data.medal_info.is_lighted === 1,
|
||||
fans_medal_wearing_status: data.medal_info !== null || data.medal_info !== undefined,
|
||||
uface: data.user_info.face.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
@@ -140,7 +153,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onGuard(command: any): void {
|
||||
public onGuard(command: MessageData.GUARD_BUY): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.guard?.forEach((d) => {
|
||||
d(data, command)
|
||||
@@ -168,9 +181,9 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onEnter(command: any): void {
|
||||
const data = command.data
|
||||
const msgType = data.msg_type
|
||||
public onEnter(command: MessageData.INTERACT_WORD_V2): void {
|
||||
const data = command.decoded
|
||||
const msgType = data?.msgType
|
||||
|
||||
if (msgType === 1) {
|
||||
this.eventsRaw?.enter?.forEach((d) => {
|
||||
@@ -180,19 +193,19 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Enter,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
uname: data?.uname || '',
|
||||
uid: this.convertToNumber(data?.uid) || 0,
|
||||
msg: '',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: data.timestamp ? data.timestamp * 1000 : Date.now(),
|
||||
guard_level: data.privilege_type || GuardLevel.None,
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
time: data?.timestamp ? this.convertToNumber(data.timestamp) * 1000 : Date.now(),
|
||||
guard_level: this.convertToNumber(data?.privilegeType) || GuardLevel.None,
|
||||
fans_medal_level: this.convertToNumber(data?.fansMedal?.medalLevel) || 0,
|
||||
fans_medal_name: data?.fansMedal?.medalName || '',
|
||||
fans_medal_wearing_status: data?.fansMedal?.isLighted === 1,
|
||||
uface: data?.uinfo?.uheadFrame?.frameImg?.replace('http://', 'https://') || (AVATAR_URL + this.convertToNumber(data?.uid)),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
ouid: GuidUtils.numToGuid(this.convertToNumber(data?.uid)),
|
||||
},
|
||||
command,
|
||||
)
|
||||
@@ -205,19 +218,19 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Follow,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
uname: data?.uname || '',
|
||||
uid: this.convertToNumber(data?.uid),
|
||||
msg: '关注了主播',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: data.timestamp ? data.timestamp * 1000 : Date.now(),
|
||||
guard_level: data.privilege_type || GuardLevel.None,
|
||||
fans_medal_level: data.fans_medal?.medal_level || 0,
|
||||
fans_medal_name: data.fans_medal?.medal_name || '',
|
||||
fans_medal_wearing_status: data.fans_medal?.is_lighted === 1,
|
||||
uface: data.face?.replace('http://', 'https://') || (AVATAR_URL + data.uid),
|
||||
time: data?.timestamp ? this.convertToNumber(data.timestamp) * 1000 : Date.now(),
|
||||
guard_level: this.convertToNumber(data?.privilegeType) || GuardLevel.None,
|
||||
fans_medal_level: this.convertToNumber(data?.fansMedal?.medalLevel) || 0,
|
||||
fans_medal_name: data?.fansMedal?.medalName || '',
|
||||
fans_medal_wearing_status: data?.fansMedal?.isLighted === 1,
|
||||
uface: data?.uinfo?.uheadFrame?.frameImg?.replace('http://', 'https://') || (AVATAR_URL + data?.uid),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
ouid: GuidUtils.numToGuid(this.convertToNumber(data?.uid)),
|
||||
},
|
||||
command,
|
||||
)
|
||||
@@ -225,6 +238,41 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
}
|
||||
}
|
||||
|
||||
convertToNumber(value: number | Long | null | undefined): number {
|
||||
if (value instanceof Long) {
|
||||
return value.toNumber()
|
||||
}
|
||||
return value || 0
|
||||
}
|
||||
|
||||
public onLike(command: any): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.like?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.like?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Like,
|
||||
uname: data.uname,
|
||||
uid: data.uid,
|
||||
msg: '为直播间点赞',
|
||||
price: 0,
|
||||
num: 1,
|
||||
time: Date.now(),
|
||||
guard_level: 0,
|
||||
fans_medal_level: data.medal_info?.medal_level ?? 0,
|
||||
fans_medal_name: data.medal_info?.medal_name ?? '',
|
||||
fans_medal_wearing_status: data.medal_info?.is_lighted === 1,
|
||||
uface: data.uface.replace('http://', 'https://'),
|
||||
open_id: '',
|
||||
ouid: GuidUtils.numToGuid(data.uid),
|
||||
},
|
||||
command,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data
|
||||
this.eventsRaw?.scDel?.forEach((d) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenLiveInfo } from '@/api/api-models'
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { LiveWS } from 'bilibili-live-danmaku'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import { EventDataTypes } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
@@ -41,17 +41,35 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
protected async initClient(): Promise<{ success: boolean, message: string }> {
|
||||
const auth = await this.getAuthInfo()
|
||||
if (auth.data) {
|
||||
const chatClient = new KeepLiveWS(auth.data.anchor_info.room_id, {
|
||||
const chatClient = new LiveWS(auth.data.anchor_info.room_id, {
|
||||
authBody: JSON.parse(auth.data.websocket_info.auth_body),
|
||||
address: auth.data.websocket_info.wss_link[0],
|
||||
})
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_DM', cmd => this.onDanmaku(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GIFT', cmd => this.onGift(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_GUARD', cmd => this.onGuard(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SC', cmd => this.onSC(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER', cmd => this.onEnter(cmd))
|
||||
chatClient.on('LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL', cmd => this.onScDel(cmd))
|
||||
chatClient.on('live', () => {
|
||||
chatClient.addEventListener('MESSAGE', cmd => {
|
||||
switch (cmd.data.cmd as string) {
|
||||
case 'LIVE_OPEN_PLATFORM_DM':
|
||||
this.onDanmaku(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_GIFT':
|
||||
this.onGift(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_GUARD':
|
||||
this.onGuard(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SC':
|
||||
this.onSC(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER':
|
||||
this.onEnter(cmd.data)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL':
|
||||
this.onScDel(cmd.data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
chatClient.addEventListener('CONNECT_SUCCESS', () => {
|
||||
console.log(
|
||||
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`,
|
||||
)
|
||||
@@ -270,6 +288,10 @@ export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
})
|
||||
}
|
||||
|
||||
public onLike(_command: any): void {
|
||||
// OpenLiveClient does not support like events
|
||||
}
|
||||
|
||||
public onScDel(command: any): void {
|
||||
const data = command.data as SCDelInfo
|
||||
this.eventsRaw.scDel?.forEach((d) => {
|
||||
|
||||
Reference in New Issue
Block a user