import { BackgroundArgs, Project } from "../project/index"; import Jessibuca from "./../libs/jessibuca"; import { MessageActionType, WrapSocketSender } from "./WrapSocketSender"; import Global from "../global"; import { isIPhone } from "../utils/device.js"; type DefaultJessibucaConfig = Omit; const DefaultOptions: DefaultJessibucaConfig = { videoBuffer: 0.2, isResize: false, useWCS: isIPhone() ? false : true, useMSE: isIPhone() ? false : true, // text: "", loadingText: "加载中,请稍候...", debug: false, supportDblclickFullscreen: true, showBandwidth: false, // 显示网速 operateBtns: { fullscreen: false, screenshot: false, play: false, audio: false, }, forceNoOffscreen: true, isNotMute: true, timeout: 120, isFlv: true, }; type TTSArgs = { speed: number; style_name: string; voice_name: string; }; export class Meta2D { videoPlayer: Jessibuca; project: Project; backgroundInfo: BackgroundArgs; projectName: string; private _sender: WrapSocketSender; heartbeat?: Heartbeat; private _isReconnecting: boolean; private _retryCount: number; jbOptions: Jessibuca.Config; constructor(project: Project, videoPlayer: Jessibuca) { this.videoPlayer = videoPlayer; this.project = project; this._sender = new WrapSocketSender(videoPlayer); if (this.project.info.backgroundInfo) { this.setBackground(this.project.info.backgroundInfo); } } static initContainerSize(dom: HTMLElement, options) { // Video 的宽高是固定的, 项目是多少就是多少无法改变。 let renderWidth = options.width || window.innerWidth; let renderHeight = Math.floor(renderWidth / 0.5625); if (renderWidth % 2 !== 0) renderWidth += 1; if (renderHeight % 2 !== 0) renderHeight += 1; dom.style.width = `${renderWidth}px`; dom.style.height = `${renderHeight}px`; return dom; } static fromProject( project: Project, options: Jessibuca.Config = {} as Jessibuca.Config ) { if (!options.container) { throw new Error("container is required"); } if (!(options.container instanceof HTMLElement)) { throw new Error("container must be an HTML element"); } options.container = Meta2D.initContainerSize(options.container, options); let jessibuca = new Jessibuca({ ...DefaultOptions, ...options, }); let meta2d = new Meta2D(project, jessibuca); meta2d.jbOptions = options; meta2d._bindPlayerEvents(jessibuca); return meta2d; } private _bindPlayerEvents(jessibuca: Jessibuca) { jessibuca.on("stats", (stats) => { if (!window.showStats) return; console.log(`stats`, stats); }); jessibuca.on("kBps", (kBps) => { if (!window.showStats) return; console.log(`kBps`, kBps); }); jessibuca.on("performance", (performance) => { if (!window.showStats) return; var show = "卡顿"; if (performance === 2) { show = "非常流畅"; } else if (performance === 1) { show = "流畅"; } console.warn("performance", show); }); jessibuca.on("error", (error) => { console.error("error", error); if ( error === "streamEnd" || error === jessibuca.ERROR.fetchError || error === jessibuca.ERROR.websocketError ) { // 这里统一的做重连。 this._sender.emit("error", error); if (this.jbOptions.autoReconnect) { this.reconnect(error); } } }); } setGlobalOptions(options) { Global.env = "private"; if (options.STREAM_SERVER) { Global.STREAM_SERVER = options.STREAM_SERVER; } } private _startHeartbeat() { this._stopHeartbeat(); this.heartbeat = new Heartbeat(this._sender, 30); } private _stopHeartbeat() { if (this.heartbeat) { this.heartbeat.clear(); this.heartbeat = undefined; } } async setBackground(BackgroundArgs: BackgroundArgs) { this.backgroundInfo = BackgroundArgs; } async play(name: string) { this.projectName = name; let url = ""; if (Global.env === "private") { url = Global.STREAM_SERVER; } else { const encodedToken = encodeURIComponent(this.project.token); url = `${Global.STREAM_SERVER}?auth=${encodedToken}`; } await this.videoPlayer.play(url); this._sender.init(); this._startHeartbeat(); await new Promise((resolve) => { const onMessage = (event) => { if (!(event.data instanceof ArrayBuffer)) { const result = JSON.parse(event.data); if (result.action === MessageActionType.Load) { this._sender.videoSocket.removeEventListener("message", onMessage); this._sender.send(MessageActionType.Start); resolve(); } } }; this._sender.videoSocket.addEventListener("message", onMessage); this._sender.send(MessageActionType.Load, { project: name, background: this.backgroundInfo, }); }); } get isMuted() { return this.videoPlayer.isMute(); } mute() { this.videoPlayer.mute(); } audioResume() { this.videoPlayer.cancelMute(); this.videoPlayer.audioResume(); } async reconnect(reason?: any) { if (this._isReconnecting) return; this._isReconnecting = true; this._retryCount++; console.warn( `[Meta2D] reconnect start, reason=`, reason, `retry=${this._retryCount}` ); // === 1. 清理旧资源 === this._stopHeartbeat(); const options = this.jbOptions || {}; this._sender.detach(); this.videoPlayer.destroy(); // === 2. 退避重连 === const delay = Math.min(30000, 1000 * 2 ** this._retryCount); await new Promise((r) => setTimeout(r, delay)); try { // === 3. 重新 play === this.videoPlayer = new Jessibuca({ ...DefaultOptions, ...options, }); this._bindPlayerEvents(this.videoPlayer); this._sender.updateJessibuca(this.videoPlayer); await this.play(this.projectName); this._startHeartbeat(); this._retryCount = 0; this._isReconnecting = false; console.warn("[Meta2D] reconnect success"); } catch (err) { console.error("[Meta2D] reconnect failed", err); this._isReconnecting = false; this.reconnect(err); } } speak(text: string, tts: TTSArgs = {} as TTSArgs) { let _tts = { silence_type: "Tailing", silence_value: "100ms", ...(this.project.info.tts_args || {}), ...tts, }; this._sender.send(MessageActionType.TTS, { text, tts_args: JSON.stringify(_tts), }); } interrupt() { this._sender.send(MessageActionType.Interrupt); } resize() { this.videoPlayer.resize(); } destroy() { this._stopHeartbeat(); this.videoPlayer.destroy(); } on(type: string, listener) { console.log("on", type, listener, this._sender); this._sender.addEventListener(type, listener); } off(type: string, listener) { this._sender.removeEventListener(type, listener); } } class Heartbeat { interval: number; sender: WrapSocketSender; intervalTime: number; constructor(sender: WrapSocketSender, time: number) { this.intervalTime = time; this.interval = 0; this.sender = sender; this.handleClose = this.handleClose.bind(this); this._start(); } private _start() { this.sender.videoSocket.addEventListener("close", this.handleClose); let socket = this.sender.videoSocket as WebSocket; this.interval = setInterval(() => { if (socket.readyState !== WebSocket.OPEN) { return; } this.sender.send(MessageActionType.Ping); }, this.intervalTime * 1000) as unknown as number; } handleClose() { this.clear(); } clear() { this.interval && clearInterval(this.interval); this.interval = 0; this.sender.videoSocket && this.sender.videoSocket.removeEventListener("close", this.handleClose); } }