blivechat/frontend/src/api/chat/ChatClientDirect.js
2020-09-13 16:29:49 +08:00

330 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}