mirror of
https://github.com/xfgryujk/blivechat.git
synced 2025-03-13 19:30:46 +08:00
添加前端直连B站弹幕服务器
This commit is contained in:
parent
2dbf1769e0
commit
6a64f3c795
@ -13,6 +13,7 @@
|
|||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"element-ui": "^2.9.1",
|
"element-ui": "^2.9.1",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
|
"pako": "^1.0.11",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-i18n": "^8.11.2",
|
"vue-i18n": "^8.11.2",
|
||||||
"vue-router": "^3.0.6"
|
"vue-router": "^3.0.6"
|
||||||
|
329
frontend/src/api/chat/ChatClientDirect.js
Normal file
329
frontend/src/api/chat/ChatClientDirect.js
Normal file
@ -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
|
||||||
|
}
|
@ -37,6 +37,9 @@ export default class ChatClientRelay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wsConnect () {
|
wsConnect () {
|
||||||
|
if (this.isDestroying) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
// 开发时使用localhost:12450
|
// 开发时使用localhost:12450
|
||||||
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
|
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
|
||||||
@ -77,7 +80,7 @@ export default class ChatClientRelay {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.console.log(`掉线重连中${++this.retryCount}`)
|
window.console.log(`掉线重连中${++this.retryCount}`)
|
||||||
this.wsConnect()
|
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsMessage (event) {
|
onWsMessage (event) {
|
||||||
|
19
frontend/src/api/chat/avatar.js
Normal file
19
frontend/src/api/chat/avatar.js
Normal file
@ -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
|
||||||
|
}
|
@ -32,3 +32,12 @@ export function getTimeTextHourMin (date) {
|
|||||||
let min = ('00' + date.getMinutes()).slice(-2)
|
let min = ('00' + date.getMinutes()).slice(-2)
|
||||||
return `${hour}:${min}`
|
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('')
|
||||||
|
}
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {mergeConfig, toBool, toInt} from '@/utils'
|
import {mergeConfig, toBool, toInt} from '@/utils'
|
||||||
import * as config from '@/api/config'
|
import * as config from '@/api/config'
|
||||||
import ChatClientRelay from '@/api/chat/ChatClientRelay'
|
import ChatClientDirect from '@/api/chat/ChatClientDirect'
|
||||||
|
// import ChatClientRelay from '@/api/chat/ChatClientRelay'
|
||||||
import ChatRenderer from '@/components/ChatRenderer'
|
import ChatRenderer from '@/components/ChatRenderer'
|
||||||
import * as constants from '@/components/ChatRenderer/constants'
|
import * as constants from '@/components/ChatRenderer/constants'
|
||||||
|
|
||||||
@ -71,7 +72,11 @@ export default {
|
|||||||
},
|
},
|
||||||
initChatClient() {
|
initChatClient() {
|
||||||
let roomId = parseInt(this.$route.params.roomId)
|
let roomId = parseInt(this.$route.params.roomId)
|
||||||
this.chatClient = new ChatClientRelay(roomId, this.config.autoTranslate)
|
// if () {
|
||||||
|
this.chatClient = new ChatClientDirect(roomId)
|
||||||
|
// } else {
|
||||||
|
// this.chatClient = new ChatClientRelay(roomId, this.config.autoTranslate)
|
||||||
|
// }
|
||||||
this.chatClient.onAddText = this.onAddText
|
this.chatClient.onAddText = this.onAddText
|
||||||
this.chatClient.onAddGift = this.onAddGift
|
this.chatClient.onAddGift = this.onAddGift
|
||||||
this.chatClient.onAddMember = this.onAddMember
|
this.chatClient.onAddMember = this.onAddMember
|
||||||
|
Loading…
Reference in New Issue
Block a user