mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
progessing on client
This commit is contained in:
48
package.json
48
package.json
@@ -11,22 +11,24 @@
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@microsoft/signalr-protocol-msgpack": "^8.0.7",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"@mixer/postmessage-rpc": "^1.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@vicons/fluent": "^0.12.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"@vueuse/router": "^11.2.0",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"@vueuse/router": "^12.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"bilibili-live-ws": "^6.3.1",
|
||||
"brotli-compress": "^1.3.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"easy-speech": "^2.4.0",
|
||||
"echarts": "^5.5.1",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-oxlint": "^0.11.0",
|
||||
"eslint-plugin-oxlint": "^0.14.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -37,19 +39,19 @@
|
||||
"monaco-editor": "^0.52.0",
|
||||
"music-metadata-browser": "^2.5.11",
|
||||
"peerjs": "^1.5.4",
|
||||
"pinia": "^2.2.6",
|
||||
"prettier": "^3.3.3",
|
||||
"pinia": "^2.2.8",
|
||||
"prettier": "^3.4.1",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"queue-typescript": "^1.0.1",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"uuid": "^11.0.2",
|
||||
"vite": "^5.4.10",
|
||||
"unplugin-vue-markdown": "^0.27.1",
|
||||
"uuid": "^11.0.3",
|
||||
"vite": "5.4.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "3.5.12",
|
||||
"vue": "3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-request": "^2.0.4",
|
||||
"vue-router": "^4.4.5",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-turnstile": "^1.0.11",
|
||||
"vue3-aplayer": "^1.7.3",
|
||||
"vue3-marquee": "^4.2.2",
|
||||
@@ -58,19 +60,19 @@
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@types/bun": "^1.1.13",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/obs-studio": "^2.17.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/parser": "^8.13.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vue/eslint-config-typescript": "^14.1.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"naive-ui": "^2.40.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"naive-ui": "^2.40.3",
|
||||
"stylus": "^0.64.0",
|
||||
"typescript": "^5.6.3"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
|
||||
export const isLoadingAccount = ref(true)
|
||||
const route = useRoute()
|
||||
|
||||
const { message } = createDiscreteApi(['message'])
|
||||
const cookie = useLocalStorage('JWT_Token', '')
|
||||
@@ -46,7 +47,6 @@ export async function GetSelfAccount() {
|
||||
}
|
||||
export function UpdateAccountLoop() {
|
||||
setInterval(() => {
|
||||
const route = useRoute()
|
||||
if (ACCOUNT.value && route?.name != 'question-display') {
|
||||
// 防止在问题详情页刷新
|
||||
GetSelfAccount()
|
||||
|
||||
175
src/data/DanmakuClients/BaseDanmakuClient.ts
Normal file
175
src/data/DanmakuClients/BaseDanmakuClient.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { EventModel } from '@/api/api-models'
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
|
||||
export default abstract class BaseDanmakuClient {
|
||||
constructor() {
|
||||
this.client = null
|
||||
}
|
||||
|
||||
public client: KeepLiveWS | null
|
||||
|
||||
public state: 'padding' | 'connected' | 'connecting' | 'disconnected' =
|
||||
'padding'
|
||||
|
||||
public abstract type: 'openlive' | 'direct'
|
||||
|
||||
public eventsAsModel: {
|
||||
danmaku: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
gift: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
sc: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
guard: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
} = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
|
||||
public async Start(): Promise<{ success: boolean; message: string }> {
|
||||
if (this.state == 'connected') {
|
||||
return {
|
||||
success: true,
|
||||
message: '弹幕客户端已启动'
|
||||
}
|
||||
}
|
||||
if (this.state == 'connecting') {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端正在启动'
|
||||
}
|
||||
}
|
||||
this.state = 'connecting'
|
||||
try {
|
||||
if (!this.client) {
|
||||
console.log(`[${this.type}] 正在启动弹幕客户端`)
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.state = 'connected'
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
console.warn(`[${this.type}] 弹幕客户端已被启动过`)
|
||||
this.state = 'connected'
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端已被启动过'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
message: err ? err.toString() : '未知错误'
|
||||
}
|
||||
}
|
||||
}
|
||||
public Stop() {
|
||||
if (this.state === 'disconnected') {
|
||||
return
|
||||
}
|
||||
this.state = 'disconnected'
|
||||
if (this.client) {
|
||||
console.log(`[${this.type}] 正在停止弹幕客户端`)
|
||||
this.client.close()
|
||||
} else {
|
||||
console.warn(`[${this.type}] 弹幕客户端未被启动, 忽略`)
|
||||
}
|
||||
this.eventsAsModel = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
}
|
||||
protected abstract initClient(): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
}>
|
||||
protected async initClientInner(
|
||||
chatClient: KeepLiveWS
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
let isConnected = false
|
||||
let isError = false
|
||||
let errorMsg = ''
|
||||
chatClient.on('error', (err: any) => {
|
||||
console.error(err)
|
||||
isError = true
|
||||
errorMsg = err
|
||||
})
|
||||
chatClient.on('live', () => {
|
||||
isConnected = true
|
||||
})
|
||||
chatClient.on('close', () => {
|
||||
console.log(`[${this.type}] 弹幕客户端已关闭`)
|
||||
})
|
||||
chatClient.on('msg', (cmd) => this.onRawMessage(cmd))
|
||||
|
||||
this.client = chatClient
|
||||
while (!isConnected && !isError) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
}
|
||||
if (isError) {
|
||||
this.client.close()
|
||||
this.client = null
|
||||
}
|
||||
return {
|
||||
success: !isError,
|
||||
message: errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
public onRawMessage = (command: any) => {
|
||||
this.eventsAsModel.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
}
|
||||
|
||||
public abstract onDanmaku(command: any): void
|
||||
public abstract onGift(command: any): void
|
||||
public abstract onSC(command: any): void
|
||||
public abstract onGuard(command: any): void
|
||||
public on(
|
||||
eventName: 'danmaku',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public on(
|
||||
eventName: 'gift',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public on(
|
||||
eventName: 'sc',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public on(
|
||||
eventName: 'guard',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public on(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public on(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
this.eventsAsModel[eventName] = []
|
||||
}
|
||||
this.eventsAsModel[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
public off(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (this.eventsAsModel[eventName]) {
|
||||
const index = this.eventsAsModel[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.eventsAsModel[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
65
src/data/DanmakuClients/DirectClient.ts
Normal file
65
src/data/DanmakuClients/DirectClient.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
export type DirectClientAuthInfo = {
|
||||
token: string
|
||||
roomId: number
|
||||
tokenUserId: number
|
||||
buvid: string
|
||||
}
|
||||
/** 直播间弹幕客户端, 只能在vtsuru.client环境使用
|
||||
*
|
||||
* 未实现除raw事件外的所有事件
|
||||
*/
|
||||
export default class DirectClient extends BaseDanmakuClient {
|
||||
public onDanmaku(command: any): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
public onGift(command: any): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
public onSC(command: any): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
public onGuard(command: any): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
constructor(auth: DirectClientAuthInfo) {
|
||||
super()
|
||||
this.authInfo = auth
|
||||
}
|
||||
|
||||
public type = 'direct' as const
|
||||
|
||||
public readonly authInfo: DirectClientAuthInfo
|
||||
|
||||
protected async initClient(): Promise<{ success: boolean; message: string }> {
|
||||
if (this.authInfo) {
|
||||
const chatClient = new KeepLiveWS(this.authInfo.roomId, {
|
||||
key: this.authInfo.token,
|
||||
buvid: this.authInfo.buvid,
|
||||
uid: this.authInfo.tokenUserId,
|
||||
protover: 3
|
||||
})
|
||||
|
||||
chatClient.on('live', () => {
|
||||
console.log('[DIRECT] 已连接房间: ' + this.authInfo.roomId)
|
||||
})
|
||||
/*chatClient.on('DANMU_MSG', this.onDanmaku)
|
||||
chatClient.on('SEND_GIFT', this.onGift)
|
||||
chatClient.on('GUARD_BUY', this.onGuard)
|
||||
chatClient.on('SUPER_CHAT_MESSAGE', this.onSC)
|
||||
chatClient.on('msg', (data) => {
|
||||
this.events.all?.forEach((d) => {
|
||||
d(data)
|
||||
})
|
||||
})*/
|
||||
return await super.initClientInner(chatClient)
|
||||
} else {
|
||||
console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息')
|
||||
return {
|
||||
success: false,
|
||||
message: '未提供弹幕客户端认证信息'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,284 @@
|
||||
import { EventDataTypes, EventModel, OpenLiveInfo } from '@/api/api-models'
|
||||
import { EventDataTypes, OpenLiveInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
// @ts-expect-error 忽略js错误
|
||||
import ChatClientDirectOpenLive from '@/data/chat/ChatClientDirectOpenLive.js'
|
||||
import { GuidUtils } from '@/Utils'
|
||||
import { KeepLiveWS } from 'bilibili-live-ws/browser'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import { OPEN_LIVE_API_URL } from './constants'
|
||||
import { OPEN_LIVE_API_URL } from '../constants'
|
||||
import BaseDanmakuClient from './BaseDanmakuClient'
|
||||
|
||||
export default class OpenLiveClient extends BaseDanmakuClient {
|
||||
constructor(auth?: AuthInfo) {
|
||||
super()
|
||||
this.authInfo = auth
|
||||
this.events = { danmaku: [], gift: [], sc: [], guard: [], all: [] }
|
||||
}
|
||||
|
||||
public type = 'openlive' as const
|
||||
|
||||
private timer: any | undefined
|
||||
|
||||
public authInfo: AuthInfo | undefined
|
||||
public roomAuthInfo: RoomAuthInfo | undefined
|
||||
public authCode: string | undefined
|
||||
|
||||
public events: {
|
||||
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
|
||||
gift: ((arg1: GiftInfo, arg2?: any) => void)[]
|
||||
sc: ((arg1: SCInfo, arg2?: any) => void)[]
|
||||
guard: ((arg1: GuardInfo, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
}
|
||||
|
||||
public async Start(): Promise<{ success: boolean; message: string }> {
|
||||
const result = await super.Start()
|
||||
if (result.success) {
|
||||
this.timer ??= setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result
|
||||
}
|
||||
public Stop() {
|
||||
super.Stop()
|
||||
this.events = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
}
|
||||
|
||||
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, {
|
||||
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('msg', (data) => {
|
||||
this.events.all?.forEach((d) => {
|
||||
d(data)
|
||||
})
|
||||
}) // 广播所有事件
|
||||
chatClient.on('live', () => {
|
||||
console.log(
|
||||
`[${this.type}] 已连接房间: ${auth.data?.anchor_info.room_id}`
|
||||
)
|
||||
})
|
||||
|
||||
return await super.initClientInner(chatClient)
|
||||
} else {
|
||||
console.log(`[${this.type}] 无法开启场次: ` + auth.message)
|
||||
return {
|
||||
success: false,
|
||||
message: auth.message
|
||||
}
|
||||
}
|
||||
}
|
||||
private async getAuthInfo(): Promise<{
|
||||
data: OpenLiveInfo | null
|
||||
message: string
|
||||
}> {
|
||||
try {
|
||||
const data = await QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'start',
|
||||
this.authInfo?.Code ? this.authInfo : undefined
|
||||
)
|
||||
if (data.code == 200) {
|
||||
console.log(`[${this.type}] 已获取场次信息`)
|
||||
return {
|
||||
data: data.data,
|
||||
message: ''
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
data: null,
|
||||
message: data.message
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
data: null,
|
||||
message: err?.toString() || '未知错误'
|
||||
}
|
||||
}
|
||||
}
|
||||
private sendHeartbeat() {
|
||||
if (this.state !== 'connected') {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
return
|
||||
}
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'heartbeat',
|
||||
this.authInfo
|
||||
)
|
||||
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
|
||||
query.then((data) => {
|
||||
if (data.code != 200) {
|
||||
console.error(`[${this.type}] 心跳失败, 将重新连接`)
|
||||
this.client?.close()
|
||||
this.client = null
|
||||
this.initClient()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public onDanmaku(command: any) {
|
||||
const data = command.data as DanmakuInfo
|
||||
this.events.danmaku?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.danmaku?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Message,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.msg,
|
||||
price: 0,
|
||||
num: 0,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
emoji: data.dm_type == 1 ? data.emoji_img_url : undefined,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onGift(command: any) {
|
||||
const data = command.data as GiftInfo
|
||||
const price = (data.price * data.gift_num) / 1000
|
||||
this.events.gift?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.gift?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Gift,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.gift_name,
|
||||
price: data.paid ? price : -price,
|
||||
num: data.gift_num,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onSC(command: any) {
|
||||
const data = command.data as SCInfo
|
||||
this.events.sc?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.sc?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.SC,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.message,
|
||||
price: data.rmb,
|
||||
num: 1,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onGuard(command: any) {
|
||||
const data = command.data as GuardInfo
|
||||
this.events.guard?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.guard?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Guard,
|
||||
name: data.user_info.uname,
|
||||
uid: data.user_info.uid,
|
||||
msg:
|
||||
data.guard_level == 1
|
||||
? '总督'
|
||||
: data.guard_level == 2
|
||||
? '提督'
|
||||
: data.guard_level == 3
|
||||
? '舰长'
|
||||
: '',
|
||||
price: 0,
|
||||
num: data.guard_num,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.user_info.uface,
|
||||
open_id: data.user_info.open_id,
|
||||
ouid:
|
||||
data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onEvent(
|
||||
eventName: 'danmaku',
|
||||
listener: DanmakuEventsMap['danmaku']
|
||||
): this
|
||||
public onEvent(eventName: 'gift', listener: DanmakuEventsMap['gift']): this
|
||||
public onEvent(eventName: 'sc', listener: DanmakuEventsMap['sc']): this
|
||||
public onEvent(eventName: 'guard', listener: DanmakuEventsMap['guard']): this
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (!this.events[eventName]) {
|
||||
this.events[eventName] = []
|
||||
}
|
||||
this.events[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
public offEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (this.events[eventName]) {
|
||||
const index = this.events[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.events[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export interface DanmakuInfo {
|
||||
room_id: number
|
||||
@@ -162,386 +436,3 @@ export interface DanmakuEventsMap {
|
||||
guard: (arg1: GuardInfo, arg2?: any) => void
|
||||
all: (arg1: any) => void
|
||||
}
|
||||
|
||||
export default class DanmakuClient {
|
||||
constructor(auth: AuthInfo | null) {
|
||||
this.authInfo = auth
|
||||
}
|
||||
|
||||
private client: any
|
||||
private timer: any | undefined
|
||||
private isStarting = false
|
||||
|
||||
public authInfo: AuthInfo | null
|
||||
public roomAuthInfo: RoomAuthInfo | undefined
|
||||
public authCode: string | undefined
|
||||
|
||||
public isRunning: boolean = false
|
||||
|
||||
public events: {
|
||||
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
|
||||
gift: ((arg1: GiftInfo, arg2?: any) => void)[]
|
||||
sc: ((arg1: SCInfo, arg2?: any) => void)[]
|
||||
guard: ((arg1: GuardInfo, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
} = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
public eventsAsModel: {
|
||||
danmaku: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
gift: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
sc: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
guard: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
} = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
|
||||
public async Start(): Promise<{ success: boolean; message: string }> {
|
||||
if (this.isRunning) {
|
||||
return {
|
||||
success: true,
|
||||
message: '弹幕客户端已启动'
|
||||
}
|
||||
}
|
||||
if (this.isStarting) {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端正在启动'
|
||||
}
|
||||
}
|
||||
this.isStarting = true
|
||||
try {
|
||||
if (!this.client) {
|
||||
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.isRunning = true
|
||||
this.timer ??= setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端已被启动过'
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isStarting = false
|
||||
}
|
||||
}
|
||||
public Stop() {
|
||||
if (!this.isRunning) {
|
||||
return
|
||||
}
|
||||
this.isRunning = false
|
||||
if (this.client) {
|
||||
console.log('[OPEN-LIVE] 正在停止弹幕客户端')
|
||||
this.client.stop()
|
||||
} else {
|
||||
console.warn('[OPEN-LIVE] 弹幕客户端未被启动, 忽略')
|
||||
}
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
this.events = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
this.eventsAsModel = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: []
|
||||
}
|
||||
}
|
||||
private sendHeartbeat() {
|
||||
if (!this.isRunning) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
return
|
||||
}
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'heartbeat',
|
||||
this.authInfo
|
||||
)
|
||||
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
|
||||
query.then((data) => {
|
||||
if (data.code != 200) {
|
||||
console.error('[OPEN-LIVE] 心跳失败, 将重新连接')
|
||||
this.client?.stop()
|
||||
this.client = null
|
||||
this.initClient()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public onRawMessage = (command: any) => {
|
||||
this.eventsAsModel.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
this.events.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
}
|
||||
|
||||
public onDanmaku = (command: any) => {
|
||||
const data = command.data as DanmakuInfo
|
||||
|
||||
this.events.danmaku?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.danmaku?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Message,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.msg,
|
||||
price: 0,
|
||||
num: 0,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
emoji: data.dm_type == 1 ? data.emoji_img_url : undefined,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onGift = (command: any) => {
|
||||
const data = command.data as GiftInfo
|
||||
const price = (data.price * data.gift_num) / 1000
|
||||
this.events.gift?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.gift?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Gift,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.gift_name,
|
||||
price: data.paid ? price : -price,
|
||||
num: data.gift_num,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onSC = (command: any) => {
|
||||
const data = command.data as SCInfo
|
||||
this.events.sc?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.sc?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.SC,
|
||||
name: data.uname,
|
||||
uid: data.uid,
|
||||
msg: data.message,
|
||||
price: data.rmb,
|
||||
num: 1,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.uface,
|
||||
open_id: data.open_id,
|
||||
ouid: data.open_id ?? GuidUtils.numToGuid(data.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public onGuard = (command: any) => {
|
||||
const data = command.data as GuardInfo
|
||||
this.events.guard?.forEach((d) => {
|
||||
d(data, command)
|
||||
})
|
||||
this.eventsAsModel.guard?.forEach((d) => {
|
||||
d(
|
||||
{
|
||||
type: EventDataTypes.Guard,
|
||||
name: data.user_info.uname,
|
||||
uid: data.user_info.uid,
|
||||
msg:
|
||||
data.guard_level == 1
|
||||
? '总督'
|
||||
: data.guard_level == 2
|
||||
? '提督'
|
||||
: data.guard_level == 3
|
||||
? '舰长'
|
||||
: '',
|
||||
price: 0,
|
||||
num: data.guard_num,
|
||||
time: data.timestamp,
|
||||
guard_level: data.guard_level,
|
||||
fans_medal_level: data.fans_medal_level,
|
||||
fans_medal_name: data.fans_medal_name,
|
||||
fans_medal_wearing_status: data.fans_medal_wearing_status,
|
||||
uface: data.user_info.uface,
|
||||
open_id: data.user_info.open_id,
|
||||
ouid:
|
||||
data.user_info.open_id ?? GuidUtils.numToGuid(data.user_info.uid)
|
||||
},
|
||||
command
|
||||
)
|
||||
})
|
||||
}
|
||||
public on(eventName: 'danmaku', listener: DanmakuEventsMap['danmaku']): this
|
||||
public on(eventName: 'gift', listener: DanmakuEventsMap['gift']): this
|
||||
public on(eventName: 'sc', listener: DanmakuEventsMap['sc']): this
|
||||
public on(eventName: 'guard', listener: DanmakuEventsMap['guard']): this
|
||||
public on(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (!this.events[eventName]) {
|
||||
this.events[eventName] = []
|
||||
}
|
||||
this.events[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
public onEvent(
|
||||
eventName: 'danmaku',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public onEvent(
|
||||
eventName: 'gift',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public onEvent(
|
||||
eventName: 'sc',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public onEvent(
|
||||
eventName: 'guard',
|
||||
listener: (arg1: EventModel, arg2?: any) => void
|
||||
): this
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
this.eventsAsModel[eventName] = []
|
||||
}
|
||||
this.eventsAsModel[eventName].push(listener)
|
||||
return this
|
||||
}
|
||||
public off(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (this.events[eventName]) {
|
||||
const index = this.events[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.events[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
public offEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard',
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
if (this.eventsAsModel[eventName]) {
|
||||
const index = this.eventsAsModel[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
this.eventsAsModel[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
private async initClient(): Promise<{ success: boolean; message: string }> {
|
||||
const auth = await this.getAuthInfo()
|
||||
if (auth.data) {
|
||||
const chatClient = new ChatClientDirectOpenLive(auth.data)
|
||||
//chatClient.msgHandler = this;
|
||||
chatClient.CMD_CALLBACK_MAP = this.CMD_CALLBACK_MAP
|
||||
chatClient.start()
|
||||
this.roomAuthInfo = auth.data as RoomAuthInfo
|
||||
this.client = chatClient
|
||||
console.log('[OPEN-LIVE] 已连接房间: ' + auth.data.anchor_info.room_id)
|
||||
return {
|
||||
success: true,
|
||||
message: ''
|
||||
}
|
||||
} else {
|
||||
console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message)
|
||||
return {
|
||||
success: false,
|
||||
message: auth.message
|
||||
}
|
||||
}
|
||||
}
|
||||
private async getAuthInfo(): Promise<{
|
||||
data: OpenLiveInfo | null
|
||||
message: string
|
||||
}> {
|
||||
try {
|
||||
const data = await QueryPostAPI<OpenLiveInfo>(
|
||||
OPEN_LIVE_API_URL + 'start',
|
||||
this.authInfo?.Code ? this.authInfo : undefined
|
||||
)
|
||||
if (data.code == 200) {
|
||||
console.log('[OPEN-LIVE] 已获取场次信息')
|
||||
return {
|
||||
data: data.data,
|
||||
message: ''
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
data: null,
|
||||
message: data.message
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
data: null,
|
||||
message: err?.toString() || '未知错误'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CMD_CALLBACK_MAP = {
|
||||
LIVE_OPEN_PLATFORM_DM: this.onDanmaku.bind(this),
|
||||
LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this),
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this),
|
||||
LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this),
|
||||
RAW_MESSAGE: this.onRawMessage.bind(this)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ChatClientOfficialBase, * as base from './ChatClientOfficialBase'
|
||||
import { processAvatarUrl } from './models'
|
||||
|
||||
export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
constructor(authInfo) {
|
||||
@@ -49,7 +50,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
}
|
||||
|
||||
data = {
|
||||
avatarUrl: chat.processAvatarUrl(data.uface),
|
||||
avatarUrl: processAvatarUrl(data.uface),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.uname,
|
||||
authorType: authorType,
|
||||
@@ -79,7 +80,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
|
||||
data = {
|
||||
id: data.msg_id,
|
||||
avatarUrl: chat.processAvatarUrl(data.uface),
|
||||
avatarUrl: processAvatarUrl(data.uface),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.uname,
|
||||
totalCoin: data.price,
|
||||
@@ -97,7 +98,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
let data = command.data
|
||||
data = {
|
||||
id: data.msg_id,
|
||||
avatarUrl: chat.processAvatarUrl(data.user_info.uface),
|
||||
avatarUrl: processAvatarUrl(data.user_info.uface),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.user_info.uname,
|
||||
privilegeType: data.guard_level,
|
||||
@@ -113,7 +114,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
let data = command.data
|
||||
data = {
|
||||
id: data.message_id.toString(),
|
||||
avatarUrl: chat.processAvatarUrl(data.uface),
|
||||
avatarUrl: processAvatarUrl(data.uface),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.uname,
|
||||
price: data.rmb,
|
||||
|
||||
176
src/data/chat/ChatClientDirectWeb.js
Normal file
176
src/data/chat/ChatClientDirectWeb.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as chat from './ChatClientOfficialBase'
|
||||
import * as chatModels from './models.js'
|
||||
import * as base from './ChatClientOfficialBase'
|
||||
import ChatClientOfficialBase from './ChatClientOfficialBase'
|
||||
|
||||
export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
constructor(roomId) {
|
||||
super()
|
||||
this.CMD_CALLBACK_MAP = CMD_CALLBACK_MAP
|
||||
|
||||
// 调用initRoom后初始化,如果失败,使用这里的默认值
|
||||
this.roomId = roomId
|
||||
this.roomOwnerUid = -1
|
||||
this.hostServerList = [
|
||||
{
|
||||
host: 'broadcastlv.chat.bilibili.com',
|
||||
port: 2243,
|
||||
wss_port: 443,
|
||||
ws_port: 2244
|
||||
}
|
||||
]
|
||||
this.hostServerToken = null
|
||||
this.buvid = ''
|
||||
}
|
||||
|
||||
async initRoom() {
|
||||
let res
|
||||
try {
|
||||
res = await (
|
||||
await fetch('/api/room_info?room_id=' + this.roomId, { method: 'GET' })
|
||||
).json()
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
this.roomId = res.roomId
|
||||
this.roomOwnerUid = res.ownerUid
|
||||
if (res.hostServerList.length !== 0) {
|
||||
this.hostServerList = res.hostServerList
|
||||
}
|
||||
this.hostServerToken = res.hostServerToken
|
||||
this.buvid = res.buvid
|
||||
return true
|
||||
}
|
||||
|
||||
async onBeforeWsConnect() {
|
||||
// 重连次数太多则重新init_room,保险
|
||||
let reinitPeriod = Math.max(3, (this.hostServerList || []).length)
|
||||
if (this.retryCount > 0 && this.retryCount % reinitPeriod === 0) {
|
||||
this.needInitRoom = true
|
||||
}
|
||||
return super.onBeforeWsConnect()
|
||||
}
|
||||
|
||||
getWsUrl() {
|
||||
let hostServer =
|
||||
this.hostServerList[this.retryCount % this.hostServerList.length]
|
||||
return `wss://${hostServer.host}:${hostServer.wss_port}/sub`
|
||||
}
|
||||
|
||||
sendAuth() {
|
||||
let authParams = {
|
||||
uid: 0,
|
||||
roomid: this.roomId,
|
||||
protover: 3,
|
||||
platform: 'web',
|
||||
type: 2,
|
||||
buvid: this.buvid
|
||||
}
|
||||
if (this.hostServerToken !== null) {
|
||||
authParams.key = this.hostServerToken
|
||||
}
|
||||
this.websocket.send(this.makePacket(authParams, base.OP_AUTH))
|
||||
}
|
||||
|
||||
async danmuMsgCallback(command) {
|
||||
let info = command.info
|
||||
|
||||
let roomId, medalLevel
|
||||
if (info[3]) {
|
||||
roomId = info[3][3]
|
||||
medalLevel = info[3][0]
|
||||
} else {
|
||||
roomId = medalLevel = 0
|
||||
}
|
||||
|
||||
let uid = info[2][0]
|
||||
let isAdmin = info[2][2]
|
||||
let privilegeType = info[7]
|
||||
let authorType
|
||||
if (uid === this.roomOwnerUid) {
|
||||
authorType = 3
|
||||
} else if (isAdmin) {
|
||||
authorType = 2
|
||||
} else if (privilegeType !== 0) {
|
||||
authorType = 1
|
||||
} else {
|
||||
authorType = 0
|
||||
}
|
||||
|
||||
let authorName = info[2][1]
|
||||
let content = info[1]
|
||||
let data = new chatModels.AddTextMsg({
|
||||
avatarUrl: await chat.getAvatarUrl(uid, authorName),
|
||||
timestamp: info[0][4] / 1000,
|
||||
authorName: authorName,
|
||||
authorType: authorType,
|
||||
content: content,
|
||||
privilegeType: privilegeType,
|
||||
isGiftDanmaku:
|
||||
Boolean(info[0][9]) || chat.isGiftDanmakuByContent(content),
|
||||
authorLevel: info[4][0],
|
||||
isNewbie: info[2][5] < 10000,
|
||||
isMobileVerified: Boolean(info[2][6]),
|
||||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
||||
emoticon: info[0][13].url || null
|
||||
})
|
||||
this.msgHandler.onAddText(data)
|
||||
}
|
||||
|
||||
sendGiftCallback(command) {
|
||||
let data = command.data
|
||||
let isPaidGift = data.coin_type === 'gold'
|
||||
data = new chatModels.AddGiftMsg({
|
||||
avatarUrl: chat.processAvatarUrl(data.face),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.uname,
|
||||
totalCoin: isPaidGift ? data.total_coin : 0,
|
||||
totalFreeCoin: !isPaidGift ? data.total_coin : 0,
|
||||
giftName: data.giftName,
|
||||
num: data.num
|
||||
})
|
||||
this.msgHandler.onAddGift(data)
|
||||
}
|
||||
|
||||
async guardBuyCallback(command) {
|
||||
let data = command.data
|
||||
data = new chatModels.AddMemberMsg({
|
||||
avatarUrl: await chat.getAvatarUrl(data.uid, data.username),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.username,
|
||||
privilegeType: data.guard_level
|
||||
})
|
||||
this.msgHandler.onAddMember(data)
|
||||
}
|
||||
|
||||
superChatMessageCallback(command) {
|
||||
let data = command.data
|
||||
data = new chatModels.AddSuperChatMsg({
|
||||
id: data.id.toString(),
|
||||
avatarUrl: chat.processAvatarUrl(data.user_info.face),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.user_info.uname,
|
||||
price: data.price,
|
||||
content: data.message
|
||||
})
|
||||
this.msgHandler.onAddSuperChat(data)
|
||||
}
|
||||
|
||||
superChatMessageDeleteCallback(command) {
|
||||
let ids = []
|
||||
for (let id of command.data.ids) {
|
||||
ids.push(id.toString())
|
||||
}
|
||||
let data = new chatModels.DelSuperChatMsg({ ids })
|
||||
this.msgHandler.onDelSuperChat(data)
|
||||
}
|
||||
}
|
||||
|
||||
const CMD_CALLBACK_MAP = {
|
||||
DANMU_MSG: ChatClientDirectWeb.prototype.danmuMsgCallback,
|
||||
SEND_GIFT: ChatClientDirectWeb.prototype.sendGiftCallback,
|
||||
GUARD_BUY: ChatClientDirectWeb.prototype.guardBuyCallback,
|
||||
SUPER_CHAT_MESSAGE: ChatClientDirectWeb.prototype.superChatMessageCallback,
|
||||
SUPER_CHAT_MESSAGE_DELETE:
|
||||
ChatClientDirectWeb.prototype.superChatMessageDeleteCallback
|
||||
}
|
||||
374
src/data/chat/ChatClientOfficialBase/index.js.new
Normal file
374
src/data/chat/ChatClientOfficialBase/index.js.new
Normal file
@@ -0,0 +1,374 @@
|
||||
import { BrotliDecode } from './brotli_decode'
|
||||
|
||||
import {
|
||||
setInterval,
|
||||
clearInterval,
|
||||
setTimeout,
|
||||
clearTimeout
|
||||
} from 'worker-timers'
|
||||
import * as chatModels from '../models'
|
||||
|
||||
const HEADER_SIZE = 16
|
||||
|
||||
export const WS_BODY_PROTOCOL_VERSION_NORMAL = 0
|
||||
export const WS_BODY_PROTOCOL_VERSION_HEARTBEAT = 1
|
||||
export const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
||||
export const WS_BODY_PROTOCOL_VERSION_BROTLI = 3
|
||||
|
||||
export const OP_HANDSHAKE = 0
|
||||
export const OP_HANDSHAKE_REPLY = 1
|
||||
export const OP_HEARTBEAT = 2
|
||||
export const OP_HEARTBEAT_REPLY = 3
|
||||
export const OP_SEND_MSG = 4
|
||||
export const OP_SEND_MSG_REPLY = 5
|
||||
export const OP_DISCONNECT_REPLY = 6
|
||||
export const OP_AUTH = 7
|
||||
export const OP_AUTH_REPLY = 8
|
||||
export const OP_RAW = 9
|
||||
export const OP_PROTO_READY = 10
|
||||
export const OP_PROTO_FINISH = 11
|
||||
export const OP_CHANGE_ROOM = 12
|
||||
export const OP_CHANGE_ROOM_REPLY = 13
|
||||
export const OP_REGISTER = 14
|
||||
export const OP_REGISTER_REPLY = 15
|
||||
export const OP_UNREGISTER = 16
|
||||
export const OP_UNREGISTER_REPLY = 17
|
||||
// B站业务自定义OP
|
||||
// export const MinBusinessOp = 1000
|
||||
// export const MaxBusinessOp = 10000
|
||||
|
||||
export const AUTH_REPLY_CODE_OK = 0
|
||||
export const AUTH_REPLY_CODE_TOKEN_ERROR = -101
|
||||
|
||||
const HEARTBEAT_INTERVAL = 10 * 1000
|
||||
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + (5 * 1000)
|
||||
|
||||
let textEncoder = new TextEncoder()
|
||||
let textDecoder = new TextDecoder()
|
||||
|
||||
export default class ChatClientOfficialBase {
|
||||
constructor() {
|
||||
this.CMD_CALLBACK_MAP = {}
|
||||
|
||||
this.msgHandler = chat.getDefaultMsgHandler()
|
||||
|
||||
this.needInitRoom = true
|
||||
this.websocket = null
|
||||
this.retryCount = 0
|
||||
this.totalRetryCount = 0
|
||||
this.isDestroying = false
|
||||
this.heartbeatTimerId = null
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
start() {
|
||||
this.wsConnect()
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isDestroying = true
|
||||
if (this.websocket) {
|
||||
this.websocket.close()
|
||||
}
|
||||
}
|
||||
|
||||
async initRoom() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
makePacket(data, operation) {
|
||||
if (typeof data === 'object') {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
let bodyArr = textEncoder.encode(data)
|
||||
|
||||
let headerBuf = new ArrayBuffer(HEADER_SIZE)
|
||||
let headerView = new DataView(headerBuf)
|
||||
headerView.setUint32(0, HEADER_SIZE + bodyArr.byteLength) // pack_len
|
||||
headerView.setUint16(4, HEADER_SIZE) // raw_header_size
|
||||
headerView.setUint16(6, 1) // ver
|
||||
headerView.setUint32(8, operation) // operation
|
||||
headerView.setUint32(12, 1) // seq_id
|
||||
|
||||
// 这里如果直接返回 new Blob([headerBuf, bodyArr]),在Chrome抓包会显示空包,实际上是有数据的,为了调试体验最好还是拷贝一遍
|
||||
let headerArr = new Uint8Array(headerBuf)
|
||||
let packetArr = new Uint8Array(bodyArr.length + headerArr.length)
|
||||
packetArr.set(headerArr)
|
||||
packetArr.set(bodyArr, headerArr.length)
|
||||
return packetArr
|
||||
}
|
||||
|
||||
sendAuth() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
addDebugMsg(content) {
|
||||
this.msgHandler.onDebugMsg(new chatModels.DebugMsg({ content }))
|
||||
}
|
||||
|
||||
async wsConnect() {
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
|
||||
this.addDebugMsg('Connecting')
|
||||
await this.onBeforeWsConnect()
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
|
||||
this.websocket = new WebSocket(this.getWsUrl())
|
||||
this.websocket.binaryType = 'arraybuffer'
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onclose = this.onWsClose.bind(this)
|
||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||
}
|
||||
|
||||
async onBeforeWsConnect() {
|
||||
if (!this.needInitRoom) {
|
||||
return
|
||||
}
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await this.initRoom()
|
||||
} catch (e) {
|
||||
res = false
|
||||
console.error('initRoom exception:', e)
|
||||
if (e instanceof chatModels.ChatClientFatalError) {
|
||||
this.stop()
|
||||
this.msgHandler.onFatalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
setTimeout(() => this.onWsClose(), 0)
|
||||
throw new Error('initRoom failed')
|
||||
}
|
||||
this.needInitRoom = false
|
||||
}
|
||||
|
||||
getWsUrl() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
onWsOpen() {
|
||||
this.addDebugMsg('Connected and authenticating')
|
||||
|
||||
this.sendAuth()
|
||||
if (this.heartbeatTimerId === null) {
|
||||
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||
}
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
||||
}
|
||||
|
||||
refreshReceiveTimeoutTimer() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
clearTimeout(this.receiveTimeoutTimerId)
|
||||
}
|
||||
this.receiveTimeoutTimerId = setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
||||
}
|
||||
|
||||
onReceiveTimeout() {
|
||||
console.warn('接收消息超时')
|
||||
this.addDebugMsg('Receiving message timed out')
|
||||
|
||||
this.discardWebsocket()
|
||||
}
|
||||
|
||||
discardWebsocket() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
if (this.websocket) {
|
||||
if (this.websocket.onclose) {
|
||||
setTimeout(() => this.onWsClose(), 0)
|
||||
}
|
||||
// 直接丢弃阻塞的websocket,不等onclose回调了
|
||||
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
||||
this.websocket.close()
|
||||
}
|
||||
}
|
||||
|
||||
onWsClose() {
|
||||
this.addDebugMsg('Disconnected')
|
||||
|
||||
this.websocket = null
|
||||
if (this.heartbeatTimerId) {
|
||||
clearInterval(this.heartbeatTimerId)
|
||||
this.heartbeatTimerId = null
|
||||
}
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
this.retryCount++
|
||||
this.totalRetryCount++
|
||||
console.warn(`掉线重连中 retryCount=${this.retryCount}, totalRetryCount=${this.totalRetryCount}`)
|
||||
|
||||
// 防止无限重连的保险措施。30次重连大概会断线500秒,应该够了
|
||||
if (this.totalRetryCount > 30) {
|
||||
this.stop()
|
||||
let error = new chatModels.ChatClientFatalError(
|
||||
chatModels.FATAL_ERROR_TYPE_TOO_MANY_RETRIES, 'The connection has lost too many times'
|
||||
)
|
||||
this.msgHandler.onFatalError(error)
|
||||
return
|
||||
}
|
||||
|
||||
this.delayReconnect()
|
||||
}
|
||||
|
||||
delayReconnect() {
|
||||
this.addDebugMsg(`Scheduling reconnection. The page is ${document.visibilityState === 'visible' ? 'visible' : 'invisible'}`)
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
setTimeout(this.wsConnect.bind(this), this.getReconnectInterval())
|
||||
return
|
||||
}
|
||||
|
||||
// 页面不可见就先不重连了,即使重连也会心跳超时
|
||||
let listener = () => {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
return
|
||||
}
|
||||
document.removeEventListener('visibilitychange', listener)
|
||||
this.wsConnect()
|
||||
}
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
}
|
||||
|
||||
getReconnectInterval() {
|
||||
// 不用retryCount了,防止意外的连接成功,导致retryCount重置
|
||||
let interval = Math.min(1000 + ((this.totalRetryCount - 1) * 2000), 20 * 1000)
|
||||
// 加上随机延迟,防止同时请求导致雪崩
|
||||
interval += Math.random() * 3000
|
||||
return interval
|
||||
}
|
||||
|
||||
onWsMessage(event) {
|
||||
if (!(event.data instanceof ArrayBuffer)) {
|
||||
console.warn('未知的websocket消息类型,data=', event.data)
|
||||
return
|
||||
}
|
||||
|
||||
let data = new Uint8Array(event.data)
|
||||
this.parseWsMessage(data)
|
||||
|
||||
// 至少成功处理1条消息
|
||||
this.retryCount = 0
|
||||
}
|
||||
|
||||
parseWsMessage(data) {
|
||||
let offset = 0
|
||||
let dataView = new DataView(data.buffer)
|
||||
let packLen = dataView.getUint32(0)
|
||||
let rawHeaderSize = dataView.getUint16(4)
|
||||
// let ver = dataView.getUint16(6)
|
||||
let operation = dataView.getUint32(8)
|
||||
// let seqId = dataView.getUint32(12)
|
||||
|
||||
switch (operation) {
|
||||
case OP_AUTH_REPLY:
|
||||
case OP_SEND_MSG_REPLY: {
|
||||
// 业务消息,可能有多个包一起发,需要分包
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
||||
this.parseBusinessMessage(dataView, body)
|
||||
|
||||
offset += packLen
|
||||
if (offset >= data.byteLength) {
|
||||
break
|
||||
}
|
||||
|
||||
dataView = new DataView(data.buffer, offset)
|
||||
packLen = dataView.getUint32(0)
|
||||
rawHeaderSize = dataView.getUint16(4)
|
||||
}
|
||||
break
|
||||
}
|
||||
case OP_HEARTBEAT_REPLY: {
|
||||
// 服务器心跳包,包含人气值,这里没用
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// 未知消息
|
||||
let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseBusinessMessage(dataView, body) {
|
||||
let ver = dataView.getUint16(6)
|
||||
let operation = dataView.getUint32(8)
|
||||
|
||||
switch (operation) {
|
||||
case OP_SEND_MSG_REPLY: {
|
||||
// 业务消息
|
||||
if (ver == WS_BODY_PROTOCOL_VERSION_BROTLI) {
|
||||
// 压缩过的先解压
|
||||
body = BrotliDecode(body)
|
||||
this.parseWsMessage(body)
|
||||
} /*else if (ver == WS_BODY_PROTOCOL_VERSION_DEFLATE) {
|
||||
// web端已经不用zlib压缩了,但是开放平台会用
|
||||
body = inflate(body)
|
||||
this.parseWsMessage(body)
|
||||
} */else {
|
||||
// 没压缩过的直接反序列化
|
||||
if (body.length !== 0) {
|
||||
try {
|
||||
body = JSON.parse(textDecoder.decode(body))
|
||||
this.handlerCommand(body)
|
||||
} catch (e) {
|
||||
console.error('body=', body)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case OP_AUTH_REPLY: {
|
||||
// 认证响应
|
||||
body = JSON.parse(textDecoder.decode(body))
|
||||
if (body.code !== AUTH_REPLY_CODE_OK) {
|
||||
console.error('认证响应错误,body=', body)
|
||||
this.needInitRoom = true
|
||||
this.discardWebsocket()
|
||||
throw new Error('认证响应错误')
|
||||
}
|
||||
this.sendHeartbeat()
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// 未知消息
|
||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlerCommand(command) {
|
||||
let cmd = command.cmd || ''
|
||||
let pos = cmd.indexOf(':')
|
||||
if (pos != -1) {
|
||||
cmd = cmd.substr(0, pos)
|
||||
}
|
||||
let callback = this.CMD_CALLBACK_MAP[cmd]
|
||||
if (callback) {
|
||||
callback.call(this, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
import { getUuid4Hex } from './utils'
|
||||
import * as constants from './constants'
|
||||
import { getUuid4Hex } from '../../views/obs/blivechat/utils'
|
||||
import * as constants from '../../views/obs/blivechat/constants'
|
||||
|
||||
export const DEFAULT_AVATAR_URL = 'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h'
|
||||
export function getDefaultMsgHandler() {
|
||||
let dummyFunc = () => {}
|
||||
return {
|
||||
onAddText: dummyFunc,
|
||||
onAddGift: dummyFunc,
|
||||
onAddMember: dummyFunc,
|
||||
onAddSuperChat: dummyFunc,
|
||||
onDelSuperChat: dummyFunc,
|
||||
onUpdateTranslation: dummyFunc,
|
||||
|
||||
onFatalError: dummyFunc,
|
||||
onDebugMsg: dummyFunc
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_AVATAR_URL =
|
||||
'https://i0.hdslb.com/bfs/face/member/noface.jpg@64w_64h'
|
||||
|
||||
export class AddTextMsg {
|
||||
constructor({
|
||||
@@ -124,3 +140,11 @@ export class DebugMsg {
|
||||
this.content = content
|
||||
}
|
||||
}
|
||||
export function processAvatarUrl(avatarUrl) {
|
||||
// 去掉协议,兼容HTTP、HTTPS
|
||||
let m = avatarUrl.match(/(?:https?:)?(.*)/)
|
||||
if (m) {
|
||||
avatarUrl = m[1]
|
||||
}
|
||||
return avatarUrl
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAccount } from '@/api/account'
|
||||
import DanmakuClient, { AuthInfo, RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import OpenLiveClient, {
|
||||
AuthInfo,
|
||||
RoomAuthInfo
|
||||
} from '@/data/DanmakuClients/OpenLiveClient'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -9,7 +12,7 @@ export interface BCMessage {
|
||||
}
|
||||
|
||||
export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
const danmakuClient = ref<DanmakuClient>(new DanmakuClient(null))
|
||||
const danmakuClient = ref<OpenLiveClient>(new OpenLiveClient())
|
||||
let bc: BroadcastChannel
|
||||
const isOwnedDanmakuClient = ref(false)
|
||||
const status = ref<'waiting' | 'initializing' | 'listening' | 'running'>(
|
||||
@@ -77,7 +80,9 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
async (lock) => {
|
||||
if (lock) {
|
||||
status.value = 'initializing'
|
||||
bc = new BroadcastChannel('vtsuru.danmaku.' + accountInfo.value?.id)
|
||||
bc = new BroadcastChannel(
|
||||
'vtsuru.danmaku.open-live' + accountInfo.value?.id
|
||||
)
|
||||
console.log('[DanmakuClient] 创建 BroadcastChannel: ' + bc.name)
|
||||
bc.onmessage = (event) => {
|
||||
const message: BCMessage = event.data as BCMessage
|
||||
@@ -86,7 +91,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
case 'check-client':
|
||||
sendBCMessage('response-client-status', {
|
||||
status: status.value,
|
||||
auth: authInfo.value,
|
||||
auth: authInfo.value
|
||||
})
|
||||
break
|
||||
case 'response-client-status':
|
||||
@@ -178,7 +183,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
const events = danmakuClient.value.events
|
||||
const eventsAsModel = danmakuClient.value.eventsAsModel
|
||||
|
||||
danmakuClient.value = new DanmakuClient(auth || null)
|
||||
danmakuClient.value = new OpenLiveClient(auth)
|
||||
|
||||
danmakuClient.value.events = events
|
||||
danmakuClient.value.eventsAsModel = eventsAsModel
|
||||
@@ -192,7 +197,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
status: 'running',
|
||||
auth: authInfo.value
|
||||
})
|
||||
danmakuClient.value.onEvent('all', (data) => {
|
||||
danmakuClient.value.on('all', (data) => {
|
||||
sendBCMessage('on-danmaku', data)
|
||||
})
|
||||
return true
|
||||
|
||||
234
src/store/useDirectDanmakuClient.ts
Normal file
234
src/store/useDirectDanmakuClient.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useAccount } from '@/api/account'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface BCMessage {
|
||||
type: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export const useDirectDanmakuClient = defineStore('DirectDanmakuClient', () => {
|
||||
const danmakuClient = ref<OpenLiveClient>(new OpenLiveClient(null))
|
||||
let bc: BroadcastChannel
|
||||
const isOwnedDirectDanmakuClient = ref(false)
|
||||
const status = ref<'waiting' | 'initializing' | 'listening' | 'running'>(
|
||||
'waiting'
|
||||
)
|
||||
const connected = computed(
|
||||
() => status.value === 'running' || status.value === 'listening'
|
||||
)
|
||||
const authInfo = ref<RoomAuthInfo>()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
let existOtherClient = false
|
||||
let isInitializing = false
|
||||
|
||||
function on(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
if (!danmakuClient.value.events[eventName]) {
|
||||
danmakuClient.value.events[eventName] = []
|
||||
}
|
||||
danmakuClient.value.events[eventName].push(listener)
|
||||
}
|
||||
function onEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
if (!danmakuClient.value.eventsAsModel[eventName]) {
|
||||
danmakuClient.value.eventsAsModel[eventName] = []
|
||||
}
|
||||
danmakuClient.value.eventsAsModel[eventName].push(listener)
|
||||
}
|
||||
|
||||
function off(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
if (danmakuClient.value.events[eventName]) {
|
||||
const index = danmakuClient.value.events[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
danmakuClient.value.events[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function offEvent(
|
||||
eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
if (danmakuClient.value.eventsAsModel[eventName]) {
|
||||
const index =
|
||||
danmakuClient.value.eventsAsModel[eventName].indexOf(listener)
|
||||
if (index > -1) {
|
||||
danmakuClient.value.eventsAsModel[eventName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initClient(auth?: AuthInfo) {
|
||||
if (!isInitializing && !connected.value) {
|
||||
isInitializing = true
|
||||
navigator.locks.request(
|
||||
'danmakuClientInit',
|
||||
{ ifAvailable: true },
|
||||
async (lock) => {
|
||||
if (lock) {
|
||||
status.value = 'initializing'
|
||||
bc = new BroadcastChannel('vtsuru.danmaku.open-live' + accountInfo.value?.id)
|
||||
console.log('[DirectDanmakuClient] 创建 BroadcastChannel: ' + bc.name)
|
||||
bc.onmessage = (event) => {
|
||||
const message: BCMessage = event.data as BCMessage
|
||||
const data = message.data ? JSON.parse(message.data) : {}
|
||||
switch (message.type) {
|
||||
case 'check-client':
|
||||
sendBCMessage('response-client-status', {
|
||||
status: status.value,
|
||||
auth: authInfo.value,
|
||||
})
|
||||
break
|
||||
case 'response-client-status':
|
||||
switch (
|
||||
data.status //如果存在已经在运行或者正在启动的客户端, 状态设为 listening
|
||||
) {
|
||||
case 'running':
|
||||
case 'initializing':
|
||||
status.value = 'listening'
|
||||
existOtherClient = true
|
||||
authInfo.value = data.auth
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'on-danmaku':
|
||||
const danmaku = JSON.parse(data)
|
||||
switch (danmaku.cmd) {
|
||||
case 'LIVE_OPEN_PLATFORM_DM':
|
||||
danmakuClient.value.onDanmaku(danmaku)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SEND_GIFT':
|
||||
danmakuClient.value.onGift(danmaku)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_SUPER_CHAT':
|
||||
danmakuClient.value.onSC(danmaku)
|
||||
break
|
||||
case 'LIVE_OPEN_PLATFORM_GUARD':
|
||||
danmakuClient.value.onGuard(danmaku)
|
||||
break
|
||||
default:
|
||||
danmakuClient.value.onRawMessage(danmaku)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log('[DirectDanmakuClient] 正在检查客户端状态...')
|
||||
sendBCMessage('check-client')
|
||||
setTimeout(() => {
|
||||
if (!connected.value) {
|
||||
isOwnedDirectDanmakuClient.value = true
|
||||
initClientInternal(auth)
|
||||
} else {
|
||||
console.log(
|
||||
'[DirectDanmakuClient] 已存在其他页面弹幕客户端, 开始监听 BroadcastChannel...'
|
||||
)
|
||||
}
|
||||
|
||||
setInterval(checkClientStatus, 500)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
isInitializing = false
|
||||
return useDirectDanmakuClient()
|
||||
}
|
||||
function sendBCMessage(type: string, data?: any) {
|
||||
bc.postMessage({
|
||||
type,
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
function checkClientStatus() {
|
||||
if (!existOtherClient && !isOwnedDirectDanmakuClient.value) {
|
||||
//当不存在其他客户端, 且自己不是弹幕客户端
|
||||
//则自己成为新的弹幕客户端
|
||||
if (status.value != 'initializing') {
|
||||
console.log('[DirectDanmakuClient] 其他 Client 离线, 开始初始化...')
|
||||
initClientInternal()
|
||||
}
|
||||
} else {
|
||||
existOtherClient = false //假设其他客户端不存在
|
||||
sendBCMessage('check-client') //检查其他客户端是否存在
|
||||
}
|
||||
}
|
||||
|
||||
async function initClientInternal(auth?: AuthInfo) {
|
||||
status.value = 'initializing'
|
||||
await navigator.locks.request(
|
||||
'danmakuClientInitInternal',
|
||||
{
|
||||
ifAvailable: true
|
||||
},
|
||||
async (lock) => {
|
||||
if (lock) {
|
||||
// 有锁
|
||||
isOwnedDirectDanmakuClient.value = true
|
||||
const events = danmakuClient.value.events
|
||||
const eventsAsModel = danmakuClient.value.eventsAsModel
|
||||
|
||||
danmakuClient.value = new OpenLiveClient(auth || null)
|
||||
|
||||
danmakuClient.value.events = events
|
||||
danmakuClient.value.eventsAsModel = eventsAsModel
|
||||
const init = async () => {
|
||||
const result = await danmakuClient.value.Start()
|
||||
if (result.success) {
|
||||
authInfo.value = danmakuClient.value.roomAuthInfo
|
||||
status.value = 'running'
|
||||
console.log('[DirectDanmakuClient] 初始化成功')
|
||||
sendBCMessage('response-client-status', {
|
||||
status: 'running',
|
||||
auth: authInfo.value
|
||||
})
|
||||
danmakuClient.value.onEvent('all', (data) => {
|
||||
sendBCMessage('on-danmaku', data)
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
console.log(
|
||||
'[DirectDanmakuClient] 初始化失败, 5秒后重试: ' + result.message
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
while (!(await init())) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
}, 5000)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 无锁
|
||||
console.log('[DirectDanmakuClient] 正在等待其他页面弹幕客户端初始化...')
|
||||
status.value = 'listening'
|
||||
isOwnedDirectDanmakuClient.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
danmakuClient,
|
||||
isOwnedDirectDanmakuClient,
|
||||
status,
|
||||
connected,
|
||||
authInfo,
|
||||
on,
|
||||
off,
|
||||
onEvent,
|
||||
offEvent,
|
||||
initClient
|
||||
}
|
||||
})
|
||||
@@ -1,102 +1,200 @@
|
||||
import DanmakuClient from '@/data/DanmakuClient'
|
||||
import { BASE_HUB_URL } from '@/data/constants'
|
||||
import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient'
|
||||
import DirectClient, {
|
||||
DirectClientAuthInfo
|
||||
} from '@/data/DanmakuClients/DirectClient'
|
||||
import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import * as msgpack from '@microsoft/signalr-protocol-msgpack'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { format } from 'date-fns'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { compress } from 'brotli-compress'
|
||||
|
||||
export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
const cookie = useLocalStorage('JWT_Token', '')
|
||||
const route = useRoute()
|
||||
const startedAt = ref<Date>()
|
||||
|
||||
const client = new DanmakuClient(null)
|
||||
const client = ref<BaseDanmakuClient>()
|
||||
const signalRClient = ref<signalR.HubConnection>()
|
||||
|
||||
const events: string[] = []
|
||||
const isStarted = ref(false)
|
||||
let timer: any
|
||||
let signalRClient: signalR.HubConnection | null = null
|
||||
let disconnectedByServer = false
|
||||
|
||||
async function Start() {
|
||||
if (isStarted.value) {
|
||||
return
|
||||
let useCookie = false
|
||||
/**
|
||||
* 是否来自Tauri客户端
|
||||
*/
|
||||
let isFromClient = false
|
||||
const prefix = computed(() => {
|
||||
if (isFromClient) {
|
||||
return '[web-fetcher-iframe] '
|
||||
}
|
||||
return '[web-fetcher] '
|
||||
})
|
||||
async function restartDanmakuClient(
|
||||
type: 'openlive' | 'direct',
|
||||
directAuthInfo?: DirectClientAuthInfo
|
||||
) {
|
||||
console.log(prefix.value + '正在重启弹幕客户端...')
|
||||
if (
|
||||
client.value?.state === 'connected' ||
|
||||
client.value?.state === 'connecting'
|
||||
) {
|
||||
client.value.Stop()
|
||||
}
|
||||
return await connectDanmakuClient(type, directAuthInfo)
|
||||
}
|
||||
async function Start(
|
||||
type: 'openlive' | 'direct' = 'openlive',
|
||||
directAuthInfo?: DirectClientAuthInfo,
|
||||
_isFromClient: boolean = false
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
if (isStarted.value) {
|
||||
startedAt.value = new Date()
|
||||
return { success: true, message: '已启动' }
|
||||
}
|
||||
const result = await navigator.locks.request(
|
||||
'webFetcherStart',
|
||||
async () => {
|
||||
isFromClient = _isFromClient
|
||||
while (!(await connectSignalR())) {
|
||||
console.log('[WEB-FETCHER] 连接失败, 5秒后重试')
|
||||
console.log(prefix.value + '连接失败, 5秒后重试')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
}
|
||||
let result = await connectDanmakuClient(type, directAuthInfo)
|
||||
while (!result?.success) {
|
||||
console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
result = await connectDanmakuClient(type, directAuthInfo)
|
||||
}
|
||||
isStarted.value = true
|
||||
disconnectedByServer = false
|
||||
return result
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
function Stop() {
|
||||
if (!isStarted.value) {
|
||||
return
|
||||
}
|
||||
isStarted.value = false
|
||||
client.Stop()
|
||||
client.value?.Stop()
|
||||
client.value = undefined
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = undefined
|
||||
}
|
||||
signalRClient?.stop()
|
||||
signalRClient = null
|
||||
signalRClient.value?.stop()
|
||||
signalRClient.value = undefined
|
||||
startedAt.value = undefined
|
||||
}
|
||||
async function connectDanmakuClient() {
|
||||
console.log('[WEB-FETCHER] 正在连接弹幕客户端...')
|
||||
const result = await client.Start()
|
||||
if (result.success) {
|
||||
console.log('[WEB-FETCHER] 加载完成, 开始监听弹幕')
|
||||
client.onEvent('all', onGetDanmakus)
|
||||
/************* ✨ Codeium Command ⭐ *************/
|
||||
/**
|
||||
* Connects to the danmaku client based on the specified type.
|
||||
*
|
||||
* @param type - The type of danmaku client to connect, either 'openlive' or 'direct'.
|
||||
* @param directConnectInfo - Optional authentication information required when connecting to a 'direct' type client.
|
||||
* It should include a token, roomId, tokenUserId, and buvid.
|
||||
*
|
||||
* @returns A promise that resolves to an object containing a success flag and a message.
|
||||
* If the connection and client start are successful, the client starts listening to danmaku events.
|
||||
* If the connection fails or the authentication information is not provided for a 'direct' type client,
|
||||
* the function returns with a failure message.
|
||||
*/
|
||||
/****** 3431380f-29f6-41b0-801a-7f081b59b4ff *******/
|
||||
async function connectDanmakuClient(
|
||||
type: 'openlive' | 'direct',
|
||||
directConnectInfo?: {
|
||||
token: string
|
||||
roomId: number
|
||||
tokenUserId: number
|
||||
buvid: string
|
||||
}
|
||||
) {
|
||||
if (
|
||||
client.value?.state === 'connected' ||
|
||||
client.value?.state === 'connecting'
|
||||
) {
|
||||
return { success: true, message: '弹幕客户端已启动' }
|
||||
}
|
||||
console.log(prefix.value + '正在连接弹幕客户端...')
|
||||
if (!client.value) {
|
||||
//只有在没有客户端的时候才创建, 并添加事件
|
||||
if (type == 'openlive') {
|
||||
client.value = new OpenLiveClient()
|
||||
} else {
|
||||
if (!directConnectInfo) {
|
||||
return { success: false, message: '未提供弹幕客户端认证信息' }
|
||||
}
|
||||
client.value = new DirectClient(directConnectInfo)
|
||||
}
|
||||
|
||||
client.value?.on('all', (data) => onGetDanmakus(data))
|
||||
}
|
||||
|
||||
const result = await client.value?.Start()
|
||||
if (result?.success) {
|
||||
console.log(prefix.value + '加载完成, 开始监听弹幕')
|
||||
timer ??= setInterval(() => {
|
||||
sendEvents()
|
||||
}, 1500)
|
||||
} else {
|
||||
console.log('[WEB-FETCHER] 弹幕客户端启动失败: ' + result.message)
|
||||
console.log(prefix.value + '弹幕客户端启动失败: ' + result?.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
async function connectSignalR() {
|
||||
console.log('[WEB-FETCHER] 正在连接到 vtsuru 服务器...')
|
||||
console.log(prefix.value + '正在连接到 vtsuru 服务器...')
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.token, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cookie.value}`,
|
||||
Authorization: `Bearer ${cookie.value}`
|
||||
},
|
||||
skipNegotiation: true,
|
||||
transport: signalR.HttpTransportType.WebSockets,
|
||||
transport: signalR.HttpTransportType.WebSockets
|
||||
})
|
||||
.withAutomaticReconnect([0, 2000, 10000, 30000])
|
||||
.withHubProtocol(new msgpack.MessagePackHubProtocol())
|
||||
.build()
|
||||
connection.on('Disconnect', (reason: unknown) => {
|
||||
console.log('[WEB-FETCHER] 被服务器断开连接: ' + reason)
|
||||
console.log(prefix.value + '被服务器断开连接: ' + reason)
|
||||
disconnectedByServer = true
|
||||
connection.stop()
|
||||
signalRClient = null
|
||||
signalRClient.value = undefined
|
||||
})
|
||||
connection.on('ConnectClient', async () => {
|
||||
if (client.isRunning) {
|
||||
/*connection.on('ConnectClient', async () => {
|
||||
if (client?.state === 'connected') {
|
||||
return
|
||||
}
|
||||
let result = await connectDanmakuClient()
|
||||
while (!result.success) {
|
||||
console.log('[WEB-FETCHER] 弹幕客户端启动失败, 5秒后重试')
|
||||
while (!result?.success) {
|
||||
console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
result = await connectDanmakuClient()
|
||||
}
|
||||
isStarted.value = true
|
||||
disconnectedByServer = false
|
||||
})
|
||||
})*/
|
||||
|
||||
connection.onclose(reconnect)
|
||||
try {
|
||||
await connection.start()
|
||||
console.log('[WEB-FETCHER] 已连接到 vtsuru 服务器')
|
||||
signalRClient = connection
|
||||
console.log(prefix.value + '已连接到 vtsuru 服务器')
|
||||
await connection.send('Finished')
|
||||
if (isFromClient) {
|
||||
// 如果来自Tauri客户端,设置自己为VTsuru客户端
|
||||
await connection.send('SetAsVTsuruClient')
|
||||
}
|
||||
signalRClient.value = connection
|
||||
return true
|
||||
} catch (e) {
|
||||
console.log('[WEB-FETCHER] 无法连接到 vtsuru 服务器: ' + e)
|
||||
console.log(prefix.value + '无法连接到 vtsuru 服务器: ' + e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -105,8 +203,12 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await signalRClient?.start()
|
||||
console.log('[WEB-FETCHER] 已重新连接')
|
||||
await signalRClient.value?.start()
|
||||
await signalRClient.value?.send('Reconnected')
|
||||
if (isFromClient) {
|
||||
await signalRClient.value?.send('SetAsVTsuruClient')
|
||||
}
|
||||
console.log(prefix.value + '已重新连接')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
setTimeout(reconnect, 5000) // 如果连接失败,则每5秒尝试一次重新启动连接
|
||||
@@ -116,7 +218,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
events.push(command)
|
||||
}
|
||||
async function sendEvents() {
|
||||
if (signalRClient?.state !== 'Connected') {
|
||||
if (signalRClient.value?.state !== 'Connected') {
|
||||
return
|
||||
}
|
||||
let tempEvents: string[] = []
|
||||
@@ -129,15 +231,29 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
count = events.length
|
||||
}
|
||||
if (tempEvents.length > 0) {
|
||||
const result = await signalRClient?.invoke<{
|
||||
const compressed = await compress(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
Events: tempEvents.map((e) =>
|
||||
typeof e === 'string' ? e : JSON.stringify(e)
|
||||
),
|
||||
Version: '1.0.0',
|
||||
OSInfo: navigator.userAgent,
|
||||
UseCookie: useCookie
|
||||
})
|
||||
)
|
||||
)
|
||||
const result = await signalRClient.value?.invoke<{
|
||||
Success: boolean
|
||||
Message: string
|
||||
}>('UploadEvents', tempEvents, false)
|
||||
}>('UploadEventsCompressed', compressed)
|
||||
if (result?.Success) {
|
||||
events.splice(0, count)
|
||||
console.log(`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`)
|
||||
console.log(
|
||||
`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`
|
||||
)
|
||||
} else {
|
||||
console.error('[WEB-FETCHER] 上传弹幕失败: ' + result?.Message)
|
||||
console.error(prefix.value + '上传弹幕失败: ' + result?.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +261,10 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
return {
|
||||
Start,
|
||||
Stop,
|
||||
restartDanmakuClient,
|
||||
client,
|
||||
signalRClient,
|
||||
isStarted,
|
||||
startedAt
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,17 +6,15 @@ import {
|
||||
BookCoins20Filled,
|
||||
Info24Filled,
|
||||
Lottery24Filled,
|
||||
MoneyOff24Filled,
|
||||
MoreHorizontal24Filled,
|
||||
TabletSpeaker24Filled,
|
||||
VehicleShip24Filled,
|
||||
VideoAdd20Filled,
|
||||
VideoAdd20Filled
|
||||
} from '@vicons/fluent'
|
||||
import { AnalyticsSharp, Calendar, Chatbox, ListCircle, MusicalNote } from '@vicons/ionicons5'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { NButton, NCard, NDivider, NEllipsis, NFlex, NGradientText, NGrid, NGridItem, NIcon, NNumberAnimation, NSpace, NText, NTooltip } from 'naive-ui'
|
||||
import { NButton, NDivider, NEllipsis, NFlex, NGradientText, NGrid, NGridItem, NIcon, NNumberAnimation, NSpace, NText, NTooltip } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { stream } from 'xlsx'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDanmakuClient } from '@/store/useDanmakuClient';
|
||||
// @ts-ignore
|
||||
import * as constants from './blivechat/constants';
|
||||
// @ts-ignore
|
||||
import * as chatModels from './blivechat/models';
|
||||
import * as chatModels from '../../data/chat/models';
|
||||
// @ts-ignore
|
||||
import * as pronunciation from './blivechat/utils/pronunciation'
|
||||
// @ts-ignore
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useElementSize } from '@vueuse/core'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Vue3Marquee } from 'vue3-marquee'
|
||||
import { NCard, NDivider, NEmpty, NSpace, NText, useMessage } from 'naive-ui'
|
||||
import { NCard, NDivider, NEmpty, NMessageProvider, NSpace, NText, useMessage } from 'naive-ui'
|
||||
import { List } from 'linqts'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -25,6 +25,7 @@ const currentId = computed(() => {
|
||||
return props.id ?? route.query.id
|
||||
})
|
||||
|
||||
const cardRef = ref()
|
||||
const listContainerRef = ref()
|
||||
const { height, width } = useElementSize(listContainerRef)
|
||||
const itemHeight = 40
|
||||
@@ -140,7 +141,8 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="live-request-background" v-bind="$attrs">
|
||||
<NMessageProvider :to="cardRef" />
|
||||
<div ref="cardRef" class="live-request-background" v-bind="$attrs">
|
||||
<p class="live-request-header">{{ settings.obsTitle ?? '点播' }}</p>
|
||||
<NDivider class="live-request-divider">
|
||||
<p class="live-request-header-count">已有 {{ activeSongs.length ?? 0 }} 条</p>
|
||||
|
||||
@@ -1,12 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { useWebFetcher } from '@/store/useWebFetcher'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { RPC } from '@mixer/postmessage-rpc'
|
||||
import DirectClient, { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient';
|
||||
import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient';
|
||||
|
||||
const webFetcher = useWebFetcher()
|
||||
let rpc: RPC | undefined
|
||||
|
||||
type QueryData = {
|
||||
url: string;
|
||||
headers?: { [key: string]: string };
|
||||
body?: any;
|
||||
}
|
||||
|
||||
function onGetDanmaku(data: any) {
|
||||
rpc?.call('on-get-danmaku', data, false)
|
||||
}
|
||||
|
||||
let timer: any
|
||||
onMounted(async () => {
|
||||
if (window.parent) { //当是客户端组件时不自动启动, 需要客户端来启动以获取启动响应
|
||||
console.log('[web-fetcher-iframe] 当前为客户端组件')
|
||||
|
||||
rpc = new RPC({
|
||||
target: window.parent,
|
||||
serviceId: 'web-fetcher',
|
||||
origin: '*'
|
||||
})
|
||||
|
||||
rpc.expose('status', () => {
|
||||
return {
|
||||
status: webFetcher.isStarted ? 'running' : 'stopped',
|
||||
type: webFetcher.client?.type,
|
||||
roomId: webFetcher.client instanceof OpenLiveClient ?
|
||||
webFetcher.client.roomAuthInfo?.anchor_info.room_id :
|
||||
webFetcher.client instanceof DirectClient ?
|
||||
webFetcher.client.authInfo.roomId : -1,
|
||||
startedAt: webFetcher.startedAt,
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
rpc.expose('start', async (data: { type: 'openlive' | 'direct', directAuthInfo?: DirectClientAuthInfo, force: boolean }) => {
|
||||
console.log('[web-fetcher-iframe] 接收到 ' + (data.force ? '强制' : '') + '启动请求')
|
||||
if (data.force && webFetcher.isStarted) {
|
||||
webFetcher.Stop()
|
||||
}
|
||||
return await webFetcher.Start(data.type, data.directAuthInfo, true).then((result) => {
|
||||
webFetcher.client?.on('all', (data) => onGetDanmaku(data))
|
||||
})
|
||||
})
|
||||
rpc.expose('stop', () => {
|
||||
console.log('[web-fetcher-iframe] 接收到停止请求')
|
||||
webFetcher.Stop()
|
||||
})
|
||||
rpc.expose('call-hub', (data: {
|
||||
name: string;
|
||||
args: any[];
|
||||
}) => {
|
||||
return webFetcher.signalRClient?.invoke(data.name, ...data.args)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
rpc?.call('ready', {}, false)
|
||||
}, 1000);
|
||||
|
||||
console.log('[web-fetcher-iframe] RPC 初始化完成')
|
||||
}
|
||||
else {
|
||||
await webFetcher.Start()
|
||||
}
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error obs的东西
|
||||
if (!webFetcher.isStarted && window.obsstudio) {
|
||||
@@ -23,16 +87,18 @@ onMounted(async () => {
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
if (window.parent) {
|
||||
webFetcher.client?.off('all', onGetDanmaku)
|
||||
|
||||
rpc?.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="web-fetcher-status"
|
||||
:style="{
|
||||
<div class="web-fetcher-status" :style="{
|
||||
backgroundColor: webFetcher.isStarted ? '#6dc56d' : '#e34a4a',
|
||||
}"
|
||||
></div>
|
||||
}"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -45,6 +111,7 @@ onUnmounted(() => {
|
||||
animation: animated-border 3s infinite;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as models from './models'
|
||||
import * as models from '../../../data/chat/models'
|
||||
|
||||
export default {
|
||||
name: 'ImgShadow',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { DownloadConfig, UploadConfig, useAccount } from '@/api/account'
|
||||
import { DanmakuUserInfo, EventModel, SongFrom, SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { CURRENT_HOST, MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
|
||||
@@ -44,6 +43,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { clearInterval, setInterval } from 'worker-timers'
|
||||
import MusicRequestOBS from '../obs/MusicRequestOBS.vue'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
|
||||
type Music = {
|
||||
id: number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account';
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClient';
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClients/OpenLiveClient';
|
||||
import { NAlert, NButton, NCard, NDivider, NSpace } from 'naive-ui';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useAccount } from '@/api/account'
|
||||
import { OpenLiveLotteryType, OpenLiveLotteryUserInfo, UpdateLiveLotteryUsersModel } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { DanmakuInfo, GiftInfo, RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { CURRENT_HOST, LOTTERY_API_URL } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
import { Delete24Filled, Info24Filled } from '@vicons/fluent'
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
import { h, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import LiveLotteryOBS from '../obs/LiveLotteryOBS.vue'
|
||||
import { DanmakuInfo, GiftInfo, RoomAuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
|
||||
interface LotteryOption {
|
||||
resultCount: number
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Setting_Queue,
|
||||
} from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI, QueryPostAPIWithParams } from '@/api/query'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { CURRENT_HOST, QUEUE_API_URL } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
import {
|
||||
@@ -70,6 +69,7 @@ import {
|
||||
import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import QueueOBS from '../obs/QueueOBS.vue'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
|
||||
const defaultSettings = {
|
||||
keyword: '排队',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { copyToClipboard } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { EventDataTypes, EventModel } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClient'
|
||||
import { RoomAuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
import { FETCH_API, VTSURU_API_URL } from '@/data/constants'
|
||||
import { useDanmakuClient } from '@/store/useDanmakuClient'
|
||||
import { Info24Filled, Mic24Filled } from '@vicons/fluent'
|
||||
|
||||
@@ -14,25 +14,20 @@
|
||||
"baseUrl": ".",
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
, "env.d.ts", "default.d.ts", "src/data/chat/ChatClientDirectOpenLive.js", "src/views/obs/blivechat/models.js" ],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"tests/**/*.tsx",
|
||||
"env.d.ts",
|
||||
"default.d.ts",
|
||||
"src/data/chat/ChatClientDirectOpenLive.js",
|
||||
"src/data/chat/models.js"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import Markdown from 'unplugin-vue-markdown/vite'
|
||||
import caddyTls from './plugins/vite-plugin-caddy'
|
||||
import ViteMonacoPlugin from 'vite-plugin-monaco-editor'
|
||||
import { defineConfig } from 'vite'
|
||||
import monacoEditorPluginModule from 'vite-plugin-monaco-editor'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import caddyTls from './plugins/vite-plugin-caddy'
|
||||
|
||||
const isObjectWithDefaultFunction = (
|
||||
module: unknown
|
||||
@@ -43,6 +42,9 @@ export default defineConfig({
|
||||
caddyTls(),
|
||||
monacoEditorPlugin({ languageWorkers: ['css'] })
|
||||
],
|
||||
server: {
|
||||
port: 51000
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
|
||||
Reference in New Issue
Block a user