mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-19 09:43:28 +08:00
添加自定义模板SDK
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
brotli_decode.js
|
||||
pronunciation/dict*.js
|
||||
blcsdk.js
|
||||
|
||||
312
frontend/src/blcsdk.js
Normal file
312
frontend/src/blcsdk.js
Normal file
@@ -0,0 +1,312 @@
|
||||
/** @module blcsdk */
|
||||
|
||||
(function(root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], factory)
|
||||
} else {
|
||||
root.blcsdk = factory()
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function() {
|
||||
const exports = {}
|
||||
|
||||
const VERSION = '1.0.0'
|
||||
/**
|
||||
* 取SDK版本
|
||||
* @returns {string} "1.0.0"
|
||||
*/
|
||||
exports.getVersion = () => VERSION
|
||||
|
||||
// 初始化消息的Promise {promise, resolve, reject}
|
||||
let initPromise = null
|
||||
// 初始化消息,包含版本、配置等信息
|
||||
let initMsg = null
|
||||
|
||||
/**
|
||||
* 消息处理器
|
||||
* @type {MsgHandler}
|
||||
*/
|
||||
let msgHandler = null
|
||||
/**
|
||||
* 用户设置的消息处理器
|
||||
* @type {MsgHandler}
|
||||
*/
|
||||
let rawMsgHandler = null
|
||||
|
||||
/**
|
||||
* @typedef InitOptions
|
||||
* @property {boolean} noMsgDelay 去掉消息延迟,但会导致消息不平滑
|
||||
*/
|
||||
|
||||
/**
|
||||
* 初始化SDK
|
||||
*
|
||||
* 在调用除了setMsgHandler以外的其他接口之前必须先调用这个
|
||||
*/
|
||||
async function init(
|
||||
/** @type {?InitOptions} */
|
||||
{noMsgDelay = false} = {}
|
||||
) {
|
||||
if (initPromise) {
|
||||
throw new Error('Cannot call init() again')
|
||||
}
|
||||
// initPromise = Promise.withResolvers()
|
||||
initPromise = {}
|
||||
initPromise.promise = new Promise((resolve, reject) => {
|
||||
initPromise.resolve = resolve
|
||||
initPromise.reject = reject
|
||||
})
|
||||
|
||||
if (window.parent === window) {
|
||||
initPromise.reject(new Error('No parent window'))
|
||||
return initPromise.promise
|
||||
}
|
||||
|
||||
msgHandler = noMsgDelay ? new SdkMsgHandler() : new SmoothedSdkMsgHandler()
|
||||
window.addEventListener('message', onWindowMessage)
|
||||
|
||||
// 连接blivechat
|
||||
blcSendMsg('blcTemplateConnect')
|
||||
setTimeout(() => initPromise.reject(new Error('Timed out waiting for blcInit message')), 10 * 1000)
|
||||
|
||||
// 等待初始化消息
|
||||
initMsg = await initPromise.promise
|
||||
console.debug('blcsdk initialized, init_msg=', initMsg)
|
||||
}
|
||||
exports.init = init
|
||||
|
||||
/**
|
||||
* 设置消息处理器
|
||||
* @param {?MsgHandler} handler 消息处理器
|
||||
*/
|
||||
function setMsgHandler(handler) {
|
||||
rawMsgHandler = handler
|
||||
}
|
||||
exports.setMsgHandler = setMsgHandler
|
||||
|
||||
/**
|
||||
* 取blivechat前端版本
|
||||
* @returns {string} "v1.10.0-dev"
|
||||
*/
|
||||
function getBlcVersion() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return initMsg.blcVersion
|
||||
}
|
||||
exports.getBlcVersion = getBlcVersion
|
||||
|
||||
/**
|
||||
* 取blivechat前端用的SDK版本。是父窗口用的版本,不是这个包的getVersion返回值"
|
||||
* @returns {string} "1.0.0"
|
||||
*/
|
||||
function getBlcSdkVersion() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return initMsg.sdkVersion
|
||||
}
|
||||
exports.getBlcSdkVersion = getBlcSdkVersion
|
||||
|
||||
/**
|
||||
* @typedef Config
|
||||
* @property {boolean} showGiftName 显示礼物名
|
||||
* @property {boolean} mergeSimilarDanmaku 合并相似弹幕
|
||||
* @property {boolean} mergeGift 合并礼物
|
||||
* @property {number} maxNumber 最大弹幕数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 取blivechat前端房间部分配置
|
||||
* @returns {Config}
|
||||
*/
|
||||
function getConfig() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return Object.freeze(initMsg.config)
|
||||
}
|
||||
exports.getConfig = getConfig
|
||||
|
||||
function blcSendMsg(type, data = null) {
|
||||
if (window.parent === window) {
|
||||
return
|
||||
}
|
||||
let msg = { type, data }
|
||||
window.parent.postMessage(msg, '*')
|
||||
}
|
||||
|
||||
function onWindowMessage(event) {
|
||||
if (event.source !== window.parent) {
|
||||
return
|
||||
}
|
||||
|
||||
let { type, data } = event.data
|
||||
switch (type) {
|
||||
case 'blcInit':
|
||||
initPromise.resolve(data)
|
||||
break
|
||||
case 'blcAddMsg':
|
||||
msgHandler.addMsg(data)
|
||||
break
|
||||
case 'blcDelMsgs':
|
||||
msgHandler.delMsgs(data.ids)
|
||||
break
|
||||
case 'blcUpdateMsg':
|
||||
msgHandler.updateMsg(data.id, data.newValuesObj)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/** 模板消息处理器接口 */
|
||||
class MsgHandler {
|
||||
/**
|
||||
* 添加消息
|
||||
* @param {Object} msg
|
||||
*/
|
||||
addMsg(msg) {}
|
||||
|
||||
/**
|
||||
* 删除消息,主要用于撤回醒目留言
|
||||
* @param {string[]} ids 要删除的消息ID
|
||||
*/
|
||||
delMsgs(ids) {}
|
||||
|
||||
/**
|
||||
* 更新消息字段,主要用于更新翻译结果
|
||||
* @param {string} id 要更新的消息ID
|
||||
* @param {Object} newValuesObj 字段和对应的新值
|
||||
*/
|
||||
updateMsg(id, newValuesObj) {}
|
||||
}
|
||||
exports.MsgHandler = MsgHandler
|
||||
|
||||
class SdkMsgHandler extends MsgHandler {
|
||||
addMsg(msg) { this._callRawHandler('addMsg', msg) }
|
||||
delMsgs(ids) { this._callRawHandler('delMsgs', ids) }
|
||||
updateMsg(id, newValuesObj) { this._callRawHandler('updateMsg', id, newValuesObj) }
|
||||
_callRawHandler(...args) { doCallRawHandler(...args) }
|
||||
}
|
||||
|
||||
function doCallRawHandler(funcName, ...args) {
|
||||
if (!rawMsgHandler) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
let func = rawMsgHandler[funcName]
|
||||
return func.call(rawMsgHandler, ...args)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息时间间隔范围
|
||||
const MSG_MIN_INTERVAL = 80
|
||||
const MSG_MAX_INTERVAL = 1000
|
||||
|
||||
class SmoothedSdkMsgHandler extends SdkMsgHandler {
|
||||
constructor() {
|
||||
super()
|
||||
// 消息队列
|
||||
this._queue = []
|
||||
// 消费消息队列的定时器ID
|
||||
this._emitSmoothedMsgTimerId = null
|
||||
// 最近进队列的时间间隔,用来估计下次进队列的时间
|
||||
this._enqueueIntervals = []
|
||||
// 上次进队列的时间
|
||||
this._lastEnqueueTime = null
|
||||
// 估计的下次进队列时间间隔
|
||||
this._estimatedEnqueueInterval = null
|
||||
|
||||
this._boundEmitSmoothedMsgs = this._emitSmoothedMsgs.bind(this)
|
||||
}
|
||||
|
||||
_callRawHandler(funcName, ...args) {
|
||||
let msg = {funcName, args}
|
||||
this._enqueueMsg(msg)
|
||||
}
|
||||
|
||||
_enqueueMsg(msg) {
|
||||
// 估计进队列时间间隔
|
||||
if (!this._lastEnqueueTime) {
|
||||
this._lastEnqueueTime = new Date()
|
||||
} else {
|
||||
let curTime = new Date()
|
||||
let interval = curTime - this._lastEnqueueTime
|
||||
// 真实的进队列时间间隔模式大概是这样:2500, 300, 300, 300, 2500, 300, ...
|
||||
// B站消息有缓冲,会一次发多条消息。这里把波峰视为发送了一次真实的WS消息,所以要过滤掉间隔太小的
|
||||
if (interval > 1000 || this._enqueueIntervals.length < 5) {
|
||||
this._enqueueIntervals.push(interval)
|
||||
if (this._enqueueIntervals.length > 5) {
|
||||
this._enqueueIntervals.splice(0, this._enqueueIntervals.length - 5)
|
||||
}
|
||||
// 这边估计得尽量大,只要不太早把消息缓冲发完就是平滑的。有MESSAGE_MAX_INTERVAL保底,不会让消息延迟太大
|
||||
// 其实可以用单调队列求最大值,偷懒不写了
|
||||
this._estimatedEnqueueInterval = Math.max(...this._enqueueIntervals)
|
||||
}
|
||||
// 上次入队时间还是要设置,否则会太早把消息缓冲发完,然后较长时间没有新消息
|
||||
this._lastEnqueueTime = curTime
|
||||
}
|
||||
|
||||
this._queue.push(msg)
|
||||
|
||||
if (!this._emitSmoothedMsgTimerId) {
|
||||
this._emitSmoothedMsgTimerId = setTimeout(this._boundEmitSmoothedMsgs)
|
||||
}
|
||||
}
|
||||
|
||||
_emitSmoothedMsgs() {
|
||||
this._emitSmoothedMsgTimerId = null
|
||||
if (this._queue.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 估计的下次进队列剩余时间
|
||||
let estimatedNextEnqueueRemainTime = 10 * 1000
|
||||
if (this._estimatedEnqueueInterval) {
|
||||
estimatedNextEnqueueRemainTime = Math.max(this._lastEnqueueTime - new Date() + this._estimatedEnqueueInterval, 1)
|
||||
}
|
||||
// 计算发送的消息数,保证在下次进队列之前发完
|
||||
// 下次进队列之前应该发多少条消息
|
||||
let shouldEmitNum = Math.max(this._queue.length, 0)
|
||||
// 下次进队列之前最多能发多少次
|
||||
let maxCanEmitCount = estimatedNextEnqueueRemainTime / MSG_MIN_INTERVAL
|
||||
// 这次发多少条消息
|
||||
let numToEmit
|
||||
if (shouldEmitNum < maxCanEmitCount) {
|
||||
// 队列中消息数很少,每次发1条也能发完
|
||||
numToEmit = 1
|
||||
} else {
|
||||
// 每次发1条以上,保证按最快速度能发完
|
||||
numToEmit = Math.ceil(shouldEmitNum / maxCanEmitCount)
|
||||
}
|
||||
|
||||
// 发消息
|
||||
let msgs = this._queue.splice(0, numToEmit)
|
||||
for (let msg of msgs) {
|
||||
doCallRawHandler(msg.funcName, ...msg.args)
|
||||
}
|
||||
|
||||
if (this._queue.length <= 0) {
|
||||
return
|
||||
}
|
||||
// 消息没发完,计算下次发消息时间
|
||||
let sleepTime
|
||||
if (numToEmit === 1) {
|
||||
// 队列中消息数很少,随便定个[MESSAGE_MIN_INTERVAL, MESSAGE_MAX_INTERVAL]的时间
|
||||
sleepTime = estimatedNextEnqueueRemainTime / this._queue.length
|
||||
sleepTime *= 0.5 + Math.random()
|
||||
if (sleepTime > MSG_MAX_INTERVAL) {
|
||||
sleepTime = MSG_MAX_INTERVAL
|
||||
} else if (sleepTime < MSG_MIN_INTERVAL) {
|
||||
sleepTime = MSG_MIN_INTERVAL
|
||||
}
|
||||
} else {
|
||||
// 按最快速度发
|
||||
sleepTime = MSG_MIN_INTERVAL
|
||||
}
|
||||
this._emitSmoothedMsgTimerId = window.setTimeout(this._boundEmitSmoothedMsgs, sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
return exports
|
||||
}))
|
||||
@@ -16,23 +16,62 @@ import * as chatModels from '@/api/chat/models'
|
||||
import ChatRenderer from '@/components/ChatRenderer'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
|
||||
class DefaultRenderer {
|
||||
constructor(rendererVm) {
|
||||
this.addMessage = rendererVm.addMessage
|
||||
this.delMessages = rendererVm.delMessages
|
||||
this.updateMessage = rendererVm.updateMessage
|
||||
this.mergeSimilarText = rendererVm.mergeSimilarText
|
||||
this.mergeSimilarGift = rendererVm.mergeSimilarGift
|
||||
}
|
||||
|
||||
destroy() {
|
||||
let dummyFunc = () => {}
|
||||
this.addMessage = dummyFunc
|
||||
this.delMessages = dummyFunc
|
||||
this.updateMessage = dummyFunc
|
||||
this.mergeSimilarText = dummyFunc
|
||||
this.mergeSimilarGift = dummyFunc
|
||||
}
|
||||
}
|
||||
|
||||
const BLC_SDK_VERSION = '1.0.0'
|
||||
|
||||
class CustomTemplateRenderer {
|
||||
constructor(templateIframe) {
|
||||
this.templateIframe = templateIframe
|
||||
constructor(templateIframe, config) {
|
||||
this._templateIframe = templateIframe
|
||||
this._config = config
|
||||
|
||||
this._enabledSendMessageToTemplate = (type, data) => {
|
||||
let msg = { type, data }
|
||||
templateIframe.contentWindow.postMessage(msg, '*')
|
||||
}
|
||||
this._sendMessageToTemplate = () => {}
|
||||
|
||||
this._boundOnWindowMessage = this._onWindowMessage.bind(this)
|
||||
window.addEventListener('message', this._boundOnWindowMessage)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener('message', this._boundOnWindowMessage)
|
||||
|
||||
let dummyFunc = () => {}
|
||||
this._enabledSendMessageToTemplate = dummyFunc
|
||||
this._sendMessageToTemplate = dummyFunc
|
||||
}
|
||||
|
||||
addMessage(message) {
|
||||
this.sendMessageToTemplate('blcAddMsg', message)
|
||||
this._sendMessageToTemplate('blcAddMsg', message)
|
||||
}
|
||||
|
||||
delMessages(ids) {
|
||||
let data = { ids }
|
||||
this.sendMessageToTemplate('blcDelMsgs', data)
|
||||
this._sendMessageToTemplate('blcDelMsgs', data)
|
||||
}
|
||||
|
||||
updateMessage(id, newValuesObj) {
|
||||
let data = { id, newValuesObj }
|
||||
this.sendMessageToTemplate('blcUpdateMsg', data)
|
||||
this._sendMessageToTemplate('blcUpdateMsg', data)
|
||||
}
|
||||
|
||||
mergeSimilarText() {
|
||||
@@ -43,9 +82,31 @@ class CustomTemplateRenderer {
|
||||
return false
|
||||
}
|
||||
|
||||
sendMessageToTemplate(cmd, data) {
|
||||
let msg = { cmd, data }
|
||||
this.templateIframe.contentWindow.postMessage(msg, '*')
|
||||
_onWindowMessage(event) {
|
||||
if (event.source !== this._templateIframe.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
let { type } = event.data
|
||||
switch (type) {
|
||||
case 'blcTemplateConnect': {
|
||||
this._sendMessageToTemplate = this._enabledSendMessageToTemplate
|
||||
|
||||
// 发送初始化消息
|
||||
let initData = {
|
||||
blcVersion: process.env.APP_VERSION,
|
||||
sdkVersion: BLC_SDK_VERSION,
|
||||
config: {
|
||||
showGiftName: this._config.showGiftName,
|
||||
mergeSimilarDanmaku: this._config.mergeSimilarDanmaku,
|
||||
mergeGift: this._config.mergeGift,
|
||||
maxNumber: this._config.maxNumber,
|
||||
}
|
||||
}
|
||||
this._sendMessageToTemplate('blcInit', initData)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +139,7 @@ export default {
|
||||
textEmoticons: [], // 官方的文本表情(后端配置的)
|
||||
pronunciationConverter: null,
|
||||
|
||||
customStyleElement, // 仅用于样式生成器中预览样式
|
||||
customStyleElement, // 仅用于样式生成器中预览样式和使用自定义模板时
|
||||
presetCssLinkElement: null,
|
||||
|
||||
renderer: null,
|
||||
@@ -124,7 +185,11 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.renderer = !this.useCustomTemplate ? this.$refs.renderer : new CustomTemplateRenderer(this.$refs.templateIframe)
|
||||
if (this.useCustomTemplate) {
|
||||
this.renderer = new CustomTemplateRenderer(this.$refs.templateIframe, this.config)
|
||||
} else {
|
||||
this.renderer = new DefaultRenderer(this.$refs.renderer)
|
||||
}
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (this.roomKeyValue === null) {
|
||||
@@ -152,6 +217,10 @@ export default {
|
||||
if (this.presetCssLinkElement) {
|
||||
document.head.removeChild(this.presetCssLinkElement)
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onVisibilityChange() {
|
||||
|
||||
Reference in New Issue
Block a user