mirror of
https://github.com/xfgryujk/blivechat.git
synced 2025-03-13 11:20:42 +08:00
330 lines
8.2 KiB
JavaScript
330 lines
8.2 KiB
JavaScript
import {inflate} from 'pako'
|
||
import {getUuid4Hex} from '@/utils'
|
||
import * as avatar from './avatar'
|
||
|
||
const HEADER_SIZE = 16
|
||
|
||
// const WS_BODY_PROTOCOL_VERSION_NORMAL = 0
|
||
// const WS_BODY_PROTOCOL_VERSION_INT = 1 // 用于心跳包
|
||
const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
||
|
||
// const OP_HANDSHAKE = 0
|
||
// const OP_HANDSHAKE_REPLY = 1
|
||
const OP_HEARTBEAT = 2
|
||
const OP_HEARTBEAT_REPLY = 3
|
||
// const OP_SEND_MSG = 4
|
||
const OP_SEND_MSG_REPLY = 5
|
||
// const OP_DISCONNECT_REPLY = 6
|
||
const OP_AUTH = 7
|
||
const OP_AUTH_REPLY = 8
|
||
// const OP_RAW = 9
|
||
// const OP_PROTO_READY = 10
|
||
// const OP_PROTO_FINISH = 11
|
||
// const OP_CHANGE_ROOM = 12
|
||
// const OP_CHANGE_ROOM_REPLY = 13
|
||
// const OP_REGISTER = 14
|
||
// const OP_REGISTER_REPLY = 15
|
||
// const OP_UNREGISTER = 16
|
||
// const OP_UNREGISTER_REPLY = 17
|
||
// B站业务自定义OP
|
||
// const MinBusinessOp = 1000
|
||
// const MaxBusinessOp = 10000
|
||
|
||
let textEncoder = new TextEncoder()
|
||
let textDecoder = new TextDecoder()
|
||
|
||
export default class ChatClientDirect {
|
||
constructor (roomId) {
|
||
// 调用initRoom后初始化,如果失败,使用这里的默认值
|
||
this.roomId = roomId
|
||
this.roomOwnerUid = 0
|
||
this.hostServerList = [
|
||
{host: "broadcastlv.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}
|
||
]
|
||
|
||
this.onAddText = null
|
||
this.onAddGift = null
|
||
this.onAddMember = null
|
||
this.onAddSuperChat = null
|
||
this.onDelSuperChat = null
|
||
this.onUpdateTranslation = null
|
||
|
||
this.websocket = null
|
||
this.retryCount = 0
|
||
this.isDestroying = false
|
||
this.heartbeatTimerId = null
|
||
}
|
||
|
||
async start () {
|
||
await this.initRoom()
|
||
this.wsConnect()
|
||
}
|
||
|
||
stop () {
|
||
this.isDestroying = true
|
||
if (this.websocket) {
|
||
this.websocket.close()
|
||
}
|
||
}
|
||
|
||
async initRoom () {
|
||
// TODO 请求后端
|
||
}
|
||
|
||
makePacket (data, operation) {
|
||
let body = textEncoder.encode(JSON.stringify(data))
|
||
let header = new ArrayBuffer(HEADER_SIZE)
|
||
let headerView = new DataView(header)
|
||
headerView.setUint32(0, HEADER_SIZE + body.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
|
||
return new Blob([header, body])
|
||
}
|
||
|
||
sendAuth () {
|
||
let authParams = {
|
||
uid: 0,
|
||
roomid: this.roomId,
|
||
protover: 2,
|
||
platform: 'web',
|
||
clientver: '1.14.3',
|
||
type: 2
|
||
}
|
||
this.websocket.send(this.makePacket(authParams, OP_AUTH))
|
||
}
|
||
|
||
wsConnect () {
|
||
if (this.isDestroying) {
|
||
return
|
||
}
|
||
let hostServer = this.hostServerList[this.retryCount % this.hostServerList.length]
|
||
const url = `wss://${hostServer.host}:${hostServer.wss_port}/sub`
|
||
this.websocket = new WebSocket(url)
|
||
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)
|
||
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), 10 * 1000)
|
||
}
|
||
|
||
sendHeartbeat () {
|
||
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
||
}
|
||
|
||
onWsOpen () {
|
||
this.sendAuth()
|
||
}
|
||
|
||
onWsClose () {
|
||
this.websocket = null
|
||
if (this.heartbeatTimerId) {
|
||
window.clearInterval(this.heartbeatTimerId)
|
||
this.heartbeatTimerId = null
|
||
}
|
||
if (this.isDestroying) {
|
||
return
|
||
}
|
||
window.console.log(`掉线重连中${++this.retryCount}`)
|
||
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||
}
|
||
|
||
onWsMessage (event) {
|
||
this.retryCount = 0
|
||
if (!(event.data instanceof ArrayBuffer)) {
|
||
window.console.warn('未知的websocket消息:', event.data)
|
||
return
|
||
}
|
||
let data = new Uint8Array(event.data)
|
||
this.handlerMessage(data)
|
||
}
|
||
|
||
handlerMessage (data) {
|
||
let offset = 0
|
||
while (offset < data.byteLength) {
|
||
let dataView = new DataView(data.buffer, offset)
|
||
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_HEARTBEAT_REPLY: {
|
||
// 人气值没用
|
||
break
|
||
}
|
||
case OP_SEND_MSG_REPLY: {
|
||
let body = new Uint8Array(data.buffer, offset + HEADER_SIZE, packLen - HEADER_SIZE)
|
||
if (ver == WS_BODY_PROTOCOL_VERSION_DEFLATE) {
|
||
body = inflate(body)
|
||
this.handlerMessage(body)
|
||
} else {
|
||
try {
|
||
body = JSON.parse(textDecoder.decode(body))
|
||
this.handlerCommand(body)
|
||
} catch (e) {
|
||
window.console.warn('body:', body)
|
||
throw e
|
||
}
|
||
}
|
||
break
|
||
}
|
||
case OP_AUTH_REPLY: {
|
||
this.sendHeartbeat()
|
||
break
|
||
}
|
||
default: {
|
||
let body = new Uint8Array(data.buffer, offset + HEADER_SIZE, packLen - HEADER_SIZE)
|
||
window.console.warn('未知包类型:operation=', operation, body)
|
||
break
|
||
}
|
||
}
|
||
|
||
offset += packLen
|
||
}
|
||
}
|
||
|
||
handlerCommand (command) {
|
||
if (command instanceof Array) {
|
||
for (let oneCommand of command) {
|
||
this.handlerCommand(oneCommand)
|
||
}
|
||
return
|
||
}
|
||
|
||
let cmd = command.cmd || ''
|
||
let pos = cmd.indexOf(':')
|
||
if (pos != -1) {
|
||
cmd = cmd.substr(0, pos)
|
||
}
|
||
let handler = COMMAND_HANDLERS[cmd]
|
||
if (handler) {
|
||
handler.call(this, command)
|
||
}
|
||
}
|
||
|
||
async onReceiveDanmaku (command) {
|
||
if (!this.onAddText) {
|
||
return
|
||
}
|
||
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 urank = info[2][5]
|
||
let data = {
|
||
avatarUrl: await avatar.getAvatarUrl(uid),
|
||
timestamp: info[0][4] / 1000,
|
||
authorName: info[2][1],
|
||
authorType: authorType,
|
||
content: info[1],
|
||
privilegeType: privilegeType,
|
||
isGiftDanmaku: !!info[0][9],
|
||
authorLevel: info[4][0],
|
||
isNewbie: urank < 10000,
|
||
isMobileVerified: !!info[2][6],
|
||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
||
id: getUuid4Hex(),
|
||
translation: ''
|
||
}
|
||
this.onAddText(data)
|
||
}
|
||
|
||
onReceiveGift (command) {
|
||
if (!this.onAddGift) {
|
||
return
|
||
}
|
||
let data = command.data
|
||
if (data.coin_type !== 'gold') { // 丢人
|
||
return
|
||
}
|
||
|
||
data = {
|
||
id: getUuid4Hex(),
|
||
avatarUrl: avatar.processAvatarUrl(data.face),
|
||
timestamp: data.timestamp,
|
||
authorName: data.uname,
|
||
totalCoin: data.total_coin,
|
||
giftName: data.giftName,
|
||
num: data.num
|
||
}
|
||
this.onAddGift(data)
|
||
}
|
||
|
||
async onBuyGuard (command) {
|
||
if (!this.onAddMember) {
|
||
return
|
||
}
|
||
|
||
let data = command.data
|
||
data = {
|
||
id: getUuid4Hex(),
|
||
avatarUrl: await avatar.getAvatarUrl(data.uid),
|
||
timestamp: data.start_time,
|
||
authorName: data.username,
|
||
privilegeType: data.guard_level
|
||
}
|
||
this.onAddMember(data)
|
||
}
|
||
|
||
onSuperChat (command) {
|
||
if (!this.onAddSuperChat) {
|
||
return
|
||
}
|
||
|
||
let data = command.data
|
||
data = {
|
||
id: data.id,
|
||
avatarUrl: avatar.processAvatarUrl(data.user_info.face),
|
||
timestamp: data.start_time,
|
||
authorName: data.user_info.uname,
|
||
price: data.price,
|
||
content: data.message,
|
||
translation: ''
|
||
}
|
||
this.onAddSuperChat(data)
|
||
}
|
||
|
||
onSuperChatDelete (command) {
|
||
if (!this.onDelSuperChat) {
|
||
return
|
||
}
|
||
|
||
let ids = []
|
||
for (let id of command.data.ids) {
|
||
ids.push(id.toString())
|
||
}
|
||
this.onDelSuperChat({ids})
|
||
}
|
||
}
|
||
|
||
const COMMAND_HANDLERS = {
|
||
DANMU_MSG: ChatClientDirect.prototype.onReceiveDanmaku,
|
||
SEND_GIFT: ChatClientDirect.prototype.onReceiveGift,
|
||
GUARD_BUY: ChatClientDirect.prototype.onBuyGuard,
|
||
SUPER_CHAT_MESSAGE: ChatClientDirect.prototype.onSuperChat,
|
||
SUPER_CHAT_MESSAGE_DELETE: ChatClientDirect.prototype.onSuperChatDelete
|
||
}
|