Skip to content

Commit

Permalink
添加前端直连B站弹幕服务器
Browse files Browse the repository at this point in the history
  • Loading branch information
xfgryujk committed Sep 13, 2020
1 parent 2dbf176 commit 6a64f3c
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 3 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"downloadjs": "^1.4.7",
"element-ui": "^2.9.1",
"lodash": "^4.17.19",
"pako": "^1.0.11",
"vue": "^2.6.10",
"vue-i18n": "^8.11.2",
"vue-router": "^3.0.6"
Expand Down
329 changes: 329 additions & 0 deletions frontend/src/api/chat/ChatClientDirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
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
}
5 changes: 4 additions & 1 deletion frontend/src/api/chat/ChatClientRelay.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export default class ChatClientRelay {
}

wsConnect () {
if (this.isDestroying) {
return
}
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
// 开发时使用localhost:12450
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
Expand Down Expand Up @@ -77,7 +80,7 @@ export default class ChatClientRelay {
return
}
window.console.log(`掉线重连中${++this.retryCount}`)
this.wsConnect()
window.setTimeout(this.wsConnect.bind(this), 1000)
}

onWsMessage (event) {
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/api/chat/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'

export function processAvatarUrl (avatarUrl) {
// 去掉协议,兼容HTTP、HTTPS
let m = avatarUrl.match(/(?:https?:)?(.*)/)
if (m) {
avatarUrl = m[1]
}
// 缩小图片加快传输
if (!avatarUrl.endsWith('noface.gif')) {
avatarUrl += '@48w_48h'
}
return avatarUrl
}

export async function getAvatarUrl () {
// TODO 请求后端
return DEFAULT_AVATAR_URL
}
9 changes: 9 additions & 0 deletions frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ export function getTimeTextHourMin (date) {
let min = ('00' + date.getMinutes()).slice(-2)
return `${hour}:${min}`
}

export function getUuid4Hex () {
let chars = []
for (let i = 0; i < 32; i++) {
let char = Math.floor(Math.random() * 16).toString(16)
chars.push(char)
}
return chars.join('')
}
Loading

0 comments on commit 6a64f3c

Please sign in to comment.