<add>实现弹幕的获取及分类展示
This commit is contained in:
1 parent
8395406e2f
commit
bdea9bf1b6
3 files changed
+1251
-18
No files matched your search
@@ -1,3 +1,32 @@
|
||||
import { CLog } from '@/utils/logUtil';
|
||||
import { Emitter, type EventMap } from './emitter';
|
||||
import pako from 'pako';
|
||||
import {
|
||||
decodeChatMessage,
|
||||
decodeControlMessage,
|
||||
decodeEmojiChatMessage,
|
||||
decodeGiftMessage,
|
||||
decodeLikeMessage,
|
||||
decodeMemberMessage,
|
||||
decodePushFrame,
|
||||
decodeResponse,
|
||||
decodeRoomRankMessage,
|
||||
decodeRoomStatsMessage,
|
||||
decodeRoomUserSeqMessage,
|
||||
decodeSocialMessage,
|
||||
encodePushFrame
|
||||
} from './model';
|
||||
import type {
|
||||
GiftStruct,
|
||||
Message,
|
||||
RoomRankMessage_RoomRank,
|
||||
RoomUserSeqMessage_Contributor,
|
||||
Text,
|
||||
User
|
||||
} from './model';
|
||||
import { getImInfo, getLiveInfo } from './request';
|
||||
import { getSignature } from './signature';
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
* 0 - 未连接
|
||||
@@ -130,3 +159,966 @@ export enum CastMethod {
|
||||
/** 自定义消息 */
|
||||
CUSTOM = 'CustomMessage'
|
||||
}
|
||||
|
||||
/**
|
||||
* 直播间直播状态
|
||||
*/
|
||||
export enum RoomStatus {
|
||||
PREPARE = 1,
|
||||
LIVING = 2,
|
||||
PAUSE = 3,
|
||||
END = 4
|
||||
}
|
||||
/** 客户端状态 */
|
||||
enum WSRoomStatus {
|
||||
/** 未连接 */
|
||||
UNCONNECTED = 1,
|
||||
/** 正在连接 */
|
||||
CONNECTING = 2,
|
||||
/** 连接中|已连接 */
|
||||
CONNECTED = 3,
|
||||
/** 重连中 */
|
||||
RECONNECTING = 4,
|
||||
/** 已关闭 */
|
||||
CLOSED = 5
|
||||
}
|
||||
|
||||
/**
|
||||
* DyCast Event
|
||||
*/
|
||||
interface DyCastEvent extends EventMap {
|
||||
/**
|
||||
* 监听ws打开
|
||||
* @param ev
|
||||
* @returns
|
||||
*/
|
||||
open: (ev?: Event, info?: DyLiveInfo) => void;
|
||||
/**
|
||||
* 监听关闭
|
||||
* @param code
|
||||
* @param reason
|
||||
* @returns
|
||||
*/
|
||||
close: (code: number, reason: string) => void;
|
||||
/**
|
||||
* 监听错误
|
||||
* @param e
|
||||
* @returns
|
||||
*/
|
||||
error: (e: Error) => void;
|
||||
/**
|
||||
* 监听弹幕
|
||||
* @param messages
|
||||
* @returns
|
||||
*/
|
||||
message: (messages: DyMessage[]) => void;
|
||||
/** 重连中 */
|
||||
reconnecting: (count?: number, code?: DyCastCloseCode, reason?: string) => void;
|
||||
/** 重连完成 */
|
||||
reconnect: (ev?: Event) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义关闭码
|
||||
*/
|
||||
export enum DyCastCloseCode {
|
||||
/** 正常关闭 */
|
||||
NORMAL = 1000,
|
||||
/** 终端离开,可能因为服务端错误,也可能因为浏览器正从打开连接的页面跳转离开 */
|
||||
GOING_AWAY = 1001,
|
||||
/** 由于协议错误而中断连接 */
|
||||
PROTOCOL_ERROR = 1002,
|
||||
/** 接收到不允许的数据类型而断开连接 */
|
||||
UNSUPPORTED = 1003,
|
||||
/** 没有收到预期的状态码 */
|
||||
NO_STATUS = 1005,
|
||||
/** 没有处理关闭帧 */
|
||||
ABNORMAL = 1006,
|
||||
/** 应用自定义状态码 */
|
||||
/** 主播未开播 */
|
||||
LIVE_END = 4001,
|
||||
/** 连接过程错误 */
|
||||
CONNECTING_ERROR = 4002,
|
||||
/** 无法正常接收信息 */
|
||||
CANNOT_RECEIVE = 4003,
|
||||
/** 因重连关闭 */
|
||||
RECONNECTING = 4004
|
||||
}
|
||||
|
||||
// 配置
|
||||
interface DyCastOptions {
|
||||
aid?: string;
|
||||
app_name?: string;
|
||||
browser_language?: string;
|
||||
browser_name?: string;
|
||||
browser_online?: boolean;
|
||||
browser_platform?: string;
|
||||
browser_version?: string;
|
||||
compress?: string;
|
||||
cookie_enabled?: boolean;
|
||||
cursor: string;
|
||||
device_platform?: string;
|
||||
did_rule?: number;
|
||||
endpoint?: string;
|
||||
heartbeatDuration?: string;
|
||||
host?: string;
|
||||
identity?: string;
|
||||
im_path?: string;
|
||||
insert_task_id?: string;
|
||||
internal_ext: string;
|
||||
live_id?: number;
|
||||
live_reason?: string;
|
||||
need_persist_msg_count?: string;
|
||||
room_id: string;
|
||||
screen_height?: number;
|
||||
screen_width?: number;
|
||||
signature: string;
|
||||
support_wrds?: number;
|
||||
tz_name?: string;
|
||||
update_version_code?: string;
|
||||
user_unique_id: string;
|
||||
version_code?: string;
|
||||
webcast_sdk_version?: string;
|
||||
}
|
||||
|
||||
interface DyCastCursor {
|
||||
cursor?: string;
|
||||
firstCursor?: string;
|
||||
internalExt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* dycast 自定义关闭信息
|
||||
*/
|
||||
interface DyCastCloseEvent {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// 消息体类型
|
||||
enum PayloadType {
|
||||
Ack = 'ack',
|
||||
Close = 'close',
|
||||
Hb = 'hb',
|
||||
Msg = 'msg'
|
||||
}
|
||||
|
||||
/** API */
|
||||
const BASE_URL = 'wss://webcast5-ws-web-lf.douyin.com/webcast/im/push/v2/';
|
||||
|
||||
/** SDK 版本 */
|
||||
const VERSION = '1.0.14-beta.0';
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
const defaultOpts: Partial<DyCastOptions> = {
|
||||
aid: '6383',
|
||||
app_name: 'douyin_web',
|
||||
browser_language: 'zh-CN',
|
||||
browser_name: 'Mozilla',
|
||||
browser_online: true,
|
||||
browser_platform: 'Win32',
|
||||
browser_version:
|
||||
'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
compress: 'gzip',
|
||||
cookie_enabled: true,
|
||||
device_platform: 'web',
|
||||
did_rule: 3,
|
||||
endpoint: 'live_pc',
|
||||
heartbeatDuration: '0',
|
||||
host: 'https://live.douyin.com',
|
||||
identity: 'audience',
|
||||
im_path: '/webcast/im/fetch/',
|
||||
insert_task_id: '',
|
||||
live_id: 1,
|
||||
live_reason: '',
|
||||
need_persist_msg_count: '15',
|
||||
screen_height: 1080,
|
||||
screen_width: 1920,
|
||||
support_wrds: 1,
|
||||
tz_name: 'Asia/Shanghai',
|
||||
update_version_code: VERSION,
|
||||
version_code: '180800',
|
||||
webcast_sdk_version: VERSION
|
||||
};
|
||||
|
||||
export class DyCast {
|
||||
/** 房间号 */
|
||||
private roomNum: string;
|
||||
|
||||
/** 房间信息 */
|
||||
private info: DyLiveInfo;
|
||||
|
||||
// 初次连接信息
|
||||
private imInfo: DyImInfo;
|
||||
|
||||
/** WS客户端 */
|
||||
private ws: WebSocket | undefined;
|
||||
|
||||
/** 连接 url */
|
||||
private url: string | undefined;
|
||||
|
||||
// 连接状态
|
||||
private state: boolean;
|
||||
|
||||
/** 客户端状态 */
|
||||
private wsRoomStatus: WSRoomStatus;
|
||||
|
||||
/** 直播间直播状态 */
|
||||
private status: RoomStatus;
|
||||
|
||||
/** 连接配置 */
|
||||
private options: DyCastOptions | undefined;
|
||||
|
||||
// 心跳
|
||||
// 主要用于检查消息接收是否正常
|
||||
private heartbeatDuration: number = 10000;
|
||||
// 心跳次数
|
||||
private pingCount: number = 0;
|
||||
// 心跳阈值
|
||||
// 如果 heartbeatDuration ms 内心跳次数大于等于该值,证明消息接收出错
|
||||
// 即 如果 10000 ms 内都没接收到新消息,证明消息接收出错
|
||||
private downgradePingCount: number = 2;
|
||||
|
||||
private pingTimer: number | undefined = void 0;
|
||||
|
||||
// 上次接收时间
|
||||
private lastReceiveTime: number;
|
||||
|
||||
private cursor: DyCastCursor;
|
||||
|
||||
/**
|
||||
* 自定义实现的 错误信息提示
|
||||
* - 由于 dycast 的服务端并不会正确处理关闭帧
|
||||
* - 调用 websocket close 后,关闭监听返回 1006
|
||||
*/
|
||||
private closeEvent: DyCastCloseEvent;
|
||||
|
||||
/** 当前重连次数 */
|
||||
private reconnectCount: number;
|
||||
/** 最大重连尝试次数 */
|
||||
private maxReconnectCount: number;
|
||||
// 是否需要重连
|
||||
private shouldReconnect: boolean;
|
||||
// 正在重连中
|
||||
private isReconnecting: boolean;
|
||||
|
||||
// 订阅者
|
||||
private emitter: Emitter<DyCastEvent>;
|
||||
|
||||
constructor(roomNum: string) {
|
||||
// 初始化
|
||||
this.roomNum = roomNum;
|
||||
this.state = !1;
|
||||
// 10秒心跳
|
||||
this.heartbeatDuration = 10000;
|
||||
this.pingCount = 0;
|
||||
this.downgradePingCount = 2;
|
||||
this.cursor = {
|
||||
cursor: '',
|
||||
firstCursor: '',
|
||||
internalExt: ''
|
||||
};
|
||||
// 当前重连次数
|
||||
this.reconnectCount = 0;
|
||||
// 最大重连次数
|
||||
this.maxReconnectCount = 3;
|
||||
// 上一次接收消息时间
|
||||
this.lastReceiveTime = Date.now();
|
||||
// 当前客户端状态
|
||||
this.wsRoomStatus = WSRoomStatus.UNCONNECTED;
|
||||
this.shouldReconnect = !1;
|
||||
/**
|
||||
* 默认情况
|
||||
* - 即未收到预期的状态码
|
||||
*/
|
||||
this.closeEvent = { code: 1005, msg: 'CLOSE_NO_STATUS' };
|
||||
this.info = {
|
||||
roomId: '',
|
||||
uniqueId: '',
|
||||
avatar: '',
|
||||
cover: '',
|
||||
nickname: '',
|
||||
title: '',
|
||||
status: 4
|
||||
};
|
||||
this.imInfo = {};
|
||||
this.status = RoomStatus.END;
|
||||
this.emitter = new Emitter<DyCastEvent>();
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听
|
||||
* @param event
|
||||
* @param listener
|
||||
*/
|
||||
public on<K extends keyof DyCastEvent>(event: K, listener: DyCastEvent[K]) {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消监听
|
||||
* @param event
|
||||
* @param listener
|
||||
*/
|
||||
public off<K extends keyof DyCastEvent>(event: K, listener: DyCastEvent[K]) {
|
||||
this.emitter.off(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性监听
|
||||
* - 如监听打开关闭
|
||||
* @param event
|
||||
* @param listener
|
||||
*/
|
||||
public once<K extends keyof DyCastEvent>(event: K, listener: DyCastEvent[K]) {
|
||||
this.emitter.once(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接
|
||||
* @returns
|
||||
*/
|
||||
public async connect() {
|
||||
try {
|
||||
if (this.state) {
|
||||
this.emitter.emit('error', Error('已连接,请勿重复连接'));
|
||||
return;
|
||||
}
|
||||
await this.fetchConnectInfo(this.roomNum);
|
||||
const params = this.getWssParam();
|
||||
if (this.isLiving()) {
|
||||
// 连接中
|
||||
this.wsRoomStatus = WSRoomStatus.CONNECTING;
|
||||
this._connect(params);
|
||||
} else {
|
||||
// 主播未开播
|
||||
const liveStatus = this.getLiveStatus();
|
||||
this.wsRoomStatus = WSRoomStatus.CLOSED;
|
||||
this.emitter.emit('close', DyCastCloseCode.LIVE_END, liveStatus.msg);
|
||||
}
|
||||
} catch (err) {
|
||||
// 过程错误
|
||||
CLog.error('房间连接前错误 =>', err);
|
||||
// 关闭
|
||||
this.emitter.emit('close', DyCastCloseCode.CONNECTING_ERROR, '房间连接前出错');
|
||||
this._afterClose();
|
||||
// 报错
|
||||
this.emitter.emit('error', err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
*/
|
||||
public getRoomStatus() {
|
||||
return this.wsRoomStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际连接逻辑
|
||||
* @param opts
|
||||
*/
|
||||
private _connect(opts: DyCastOptions) {
|
||||
// 连接前的初始化
|
||||
this.options = opts;
|
||||
this.url = this._getSocketUrl(opts);
|
||||
this.cursor = {
|
||||
cursor: '',
|
||||
firstCursor: opts.cursor,
|
||||
internalExt: opts.internal_ext
|
||||
};
|
||||
this.lastReceiveTime = Date.now();
|
||||
this.pingCount = 0;
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.addEventListener('open', (ev: Event) => {
|
||||
// 可能初次打开,也可能是重连打开
|
||||
if (this.reconnectCount > 0) {
|
||||
// 重连成功
|
||||
this.reconnectCount = 0;
|
||||
this.emitter.emit('reconnect', ev);
|
||||
} else {
|
||||
// 初次连接
|
||||
this.emitter.emit('open', ev, this.info);
|
||||
}
|
||||
this.ping();
|
||||
this._afterOpen();
|
||||
});
|
||||
this.ws.addEventListener('close', (ev: CloseEvent) => {
|
||||
this.handleClose(ev);
|
||||
});
|
||||
this.ws.addEventListener('error', (ev: Event) => {
|
||||
this.emitter.emit('error', Error(ev.type || 'Unknown Error'));
|
||||
});
|
||||
this.ws.addEventListener('message', (ev: MessageEvent) => {
|
||||
this.handleMessage(ev.data);
|
||||
});
|
||||
} catch (err) {
|
||||
CLog.error('房间连接过程错误 =>', err);
|
||||
// 可能原因为 WebSocket 不可用
|
||||
// 关闭
|
||||
this.emitter.emit('close', DyCastCloseCode.CONNECTING_ERROR, '房间连接过程出错');
|
||||
this._afterClose();
|
||||
// 报错
|
||||
this.emitter.emit('error', err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理关闭 */
|
||||
private handleClose(ev: CloseEvent) {
|
||||
let { code, reason } = ev;
|
||||
let msg: string = reason.toString();
|
||||
switch (code) {
|
||||
case DyCastCloseCode.NO_STATUS:
|
||||
case DyCastCloseCode.ABNORMAL:
|
||||
code = this.closeEvent.code || code;
|
||||
msg = this.closeEvent.msg || msg || 'closed';
|
||||
break;
|
||||
}
|
||||
this._afterClose();
|
||||
if (this.shouldReconnect || this.reconnectCount > 0) {
|
||||
// 需要重连
|
||||
this.reconnect();
|
||||
} else {
|
||||
// 正常关闭
|
||||
this.emitter.emit('close', code, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*/
|
||||
private async handleMessage(data: ArrayBuffer) {
|
||||
this.pingCount = 0;
|
||||
this.lastReceiveTime = Date.now();
|
||||
let res;
|
||||
try {
|
||||
res = await this._decodeFrame(new Uint8Array(data));
|
||||
} catch (err) {
|
||||
res = null;
|
||||
}
|
||||
if (!res) return;
|
||||
const { response, frame, cursor, needAck, internalExt } = res;
|
||||
if (needAck) {
|
||||
// 发送 ack
|
||||
const ack = this._ack(internalExt, frame?.logId);
|
||||
this.setCursor(cursor, internalExt);
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(ack);
|
||||
} else {
|
||||
// 重连
|
||||
CLog.error(`ACK发送异常 => 直播间[${this.roomNum}]已关闭`);
|
||||
this._afterClose();
|
||||
// this.reconnect();
|
||||
}
|
||||
}
|
||||
// 处理消息体
|
||||
if (frame) {
|
||||
// 判断消息体类型
|
||||
if (frame.payloadType === PayloadType.Msg) {
|
||||
this._dealMessages(response.messages);
|
||||
}
|
||||
if (frame.payloadType === PayloadType.Close) {
|
||||
// 关闭连接
|
||||
this.close(DyCastCloseCode.NORMAL, 'Close By PayloadType');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连
|
||||
*/
|
||||
private reconnect() {
|
||||
// 还未关闭
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.close(DyCastCloseCode.RECONNECTING, '因重连而关闭');
|
||||
}
|
||||
this.shouldReconnect = !1;
|
||||
const opts: DyCastOptions = Object.assign({}, this.options, {
|
||||
cursor: this.cursor.cursor,
|
||||
internal_ext: this.cursor.internalExt
|
||||
});
|
||||
this.reconnectCount++;
|
||||
if (this.reconnectCount > this.maxReconnectCount) {
|
||||
CLog.error('已超过最大重连次数,请稍后重试');
|
||||
this.emitter.emit('error', Error('已超过最大重连次数,请稍后重试'));
|
||||
return;
|
||||
}
|
||||
this.wsRoomStatus = WSRoomStatus.RECONNECTING;
|
||||
this.emitter.emit('reconnecting', this.reconnectCount);
|
||||
this.isReconnecting = true;
|
||||
this._connect(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
public close(code: number = 1005, reason: string = 'close') {
|
||||
if (this.ws) {
|
||||
this.state = !1;
|
||||
this.closeEvent = { code, msg: reason };
|
||||
// 无需传 code,因为抖音弹幕ws服务端并不会处理关闭帧
|
||||
this.ws.close();
|
||||
this.ws = void 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳帧
|
||||
*/
|
||||
private ping() {
|
||||
try {
|
||||
let dur = Math.max(10000, Number(this.heartbeatDuration));
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
// 连接正常
|
||||
// 发送心跳 => hb
|
||||
this.ws.send(this._ping());
|
||||
this.pingCount++;
|
||||
if (this.pingCount >= this.downgradePingCount) {
|
||||
return this.cannotReceiveMessage();
|
||||
}
|
||||
}
|
||||
// 心跳:大概每 10 秒发送一次
|
||||
this.pingTimer = setTimeout(() => {
|
||||
this.state && this.ping();
|
||||
}, dur);
|
||||
} catch (err) {
|
||||
// 发送过程出错
|
||||
CLog.error('DyCast Ping Error =>', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无法正常接收消息
|
||||
* - 长时间未接收到消息
|
||||
*/
|
||||
private cannotReceiveMessage() {
|
||||
// 先关闭
|
||||
this.close(DyCastCloseCode.CANNOT_RECEIVE, '客户端无法正常接收信息');
|
||||
let tmp = Date.now() - this.lastReceiveTime;
|
||||
CLog.error(`DyCast Cannot Receive Message => after ${tmp} ms`);
|
||||
// 重连
|
||||
this.emitter.emit('reconnecting', this.reconnectCount, DyCastCloseCode.CANNOT_RECEIVE, '客户端无法正常接收信息');
|
||||
this.reconnectCount < this.maxReconnectCount && (this.shouldReconnect = !0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 cursor
|
||||
* @param cur
|
||||
* @param ext
|
||||
*/
|
||||
private setCursor(cur: string, ext: string) {
|
||||
this.cursor.cursor = cur;
|
||||
this.cursor.internalExt = ext;
|
||||
if (!this.cursor.firstCursor) {
|
||||
this.cursor.firstCursor = cur;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一次接收的消息集
|
||||
*/
|
||||
private async _dealMessages(msgs?: Message[]) {
|
||||
if (!msgs || msgs.length < 1) return;
|
||||
const messages: DyMessage[] = [];
|
||||
try {
|
||||
for (const msg of msgs) {
|
||||
const message = await this._dealMessage(msg);
|
||||
if (message) messages.push(message);
|
||||
}
|
||||
} catch (err) {}
|
||||
if (!messages.length) return;
|
||||
this.emitter.emit('message', messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一条消息
|
||||
* @param msg
|
||||
*/
|
||||
private async _dealMessage(msg: Message) {
|
||||
const method = msg.method;
|
||||
const data: DyMessage | null = {};
|
||||
data.id = msg.msgId;
|
||||
let message = null;
|
||||
let payload = msg.payload;
|
||||
if (!payload) return null;
|
||||
try {
|
||||
// 处理消息
|
||||
switch (method) {
|
||||
case CastMethod.CHAT:
|
||||
message = decodeChatMessage(payload);
|
||||
data.method = CastMethod.CHAT;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.content = message.content;
|
||||
// 获取富文本:包含合并表情
|
||||
data.rtfContent = this._getCastRtfContent(message.rtfContentV2);
|
||||
break;
|
||||
case CastMethod.GIFT:
|
||||
message = decodeGiftMessage(payload);
|
||||
data.method = CastMethod.GIFT;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.gift = this._getCastGift(message.gift, message.repeatCount || message.comboCount, message.repeatEnd);
|
||||
break;
|
||||
case CastMethod.LIKE:
|
||||
message = decodeLikeMessage(payload);
|
||||
data.method = CastMethod.LIKE;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.content = `为主播点赞了(${message.count})`;
|
||||
data.room = { likeCount: message.total };
|
||||
break;
|
||||
case CastMethod.MEMBER:
|
||||
message = decodeMemberMessage(payload);
|
||||
data.method = CastMethod.MEMBER;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.content = '进入直播间';
|
||||
data.room = { audienceCount: message.memberCount };
|
||||
break;
|
||||
case CastMethod.SOCIAL:
|
||||
message = decodeSocialMessage(payload);
|
||||
data.method = CastMethod.SOCIAL;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.content = '关注了主播';
|
||||
data.room = { followCount: message.followCount };
|
||||
break;
|
||||
case CastMethod.EMOJI_CHAT:
|
||||
message = decodeEmojiChatMessage(payload);
|
||||
data.method = CastMethod.EMOJI_CHAT;
|
||||
data.user = this._getCastUser(message.user);
|
||||
data.content = this._getCastEmoji(message.emojiContent);
|
||||
break;
|
||||
case CastMethod.ROOM_USER_SEQ:
|
||||
message = decodeRoomUserSeqMessage(payload);
|
||||
data.method = CastMethod.ROOM_USER_SEQ;
|
||||
data.rank = this._getCastRanksA(message.ranks);
|
||||
data.room = { audienceCount: message.total, totalUserCount: message.totalUser };
|
||||
break;
|
||||
case CastMethod.CONTROL:
|
||||
message = decodeControlMessage(payload);
|
||||
data.method = CastMethod.CONTROL;
|
||||
data.content = message.common?.describe;
|
||||
data.room = { status: parseInt(message.action || '') || void 0 };
|
||||
break;
|
||||
case CastMethod.ROOM_RANK:
|
||||
message = decodeRoomRankMessage(payload);
|
||||
data.method = CastMethod.ROOM_RANK;
|
||||
data.rank = this._getCastRanksB(message.ranks);
|
||||
break;
|
||||
case CastMethod.ROOM_STATS:
|
||||
message = decodeRoomStatsMessage(payload);
|
||||
data.method = CastMethod.ROOM_STATS;
|
||||
data.room = { audienceCount: message.displayMiddle };
|
||||
break;
|
||||
}
|
||||
if (!data.method) return null;
|
||||
} catch (err) {
|
||||
// MLog.error('DyCast Message Decode Error =>', method);
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的送礼榜单
|
||||
* @param data
|
||||
*/
|
||||
private _getCastRanksA(data?: RoomUserSeqMessage_Contributor[]): LiveRankItem[] | undefined {
|
||||
if (!data || !data.length) return void 0;
|
||||
const list: LiveRankItem[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
list.push({
|
||||
avatar: item.user?.avatarThumb?.urlList?.[0] || '',
|
||||
nickname: item.user?.nickname || '',
|
||||
rank: item.rank || i + 1
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的送礼榜单
|
||||
* @param data
|
||||
*/
|
||||
private _getCastRanksB(data?: RoomRankMessage_RoomRank[]): LiveRankItem[] | undefined {
|
||||
if (!data || !data.length) return void 0;
|
||||
const list: LiveRankItem[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
list.push({
|
||||
avatar: item.user?.avatarThumb?.urlList?.[0] || '',
|
||||
nickname: item.user?.nickname || '',
|
||||
rank: item.scoreStr || i + 1
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取弹幕用户
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
private _getCastUser(data?: User): CastUser | undefined {
|
||||
if (!data) return void 0;
|
||||
return {
|
||||
id: data.secUid,
|
||||
name: data.nickname,
|
||||
gender: data.gender,
|
||||
avatar: data.avatarThumb?.urlList?.[0]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取弹幕礼物
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
private _getCastGift(data?: GiftStruct, count?: string, end?: number): CastGift | undefined {
|
||||
if (!data) return void 0;
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
price: data.diamondCount,
|
||||
type: data.type,
|
||||
desc: data.describe,
|
||||
icon: data.image?.urlList?.[0],
|
||||
count: count,
|
||||
repeatEnd: end
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员表情
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
private _getCastEmoji(data?: Text): string | undefined {
|
||||
if (!data) return void 0;
|
||||
return data.pieces?.[0]?.imageValue?.image?.urlList?.[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取弹幕富文本内容
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
private _getCastRtfContent(data?: Text): CastRtfContent[] | undefined {
|
||||
if (!data) return void 0;
|
||||
if (!data.pieces) return void 0;
|
||||
const pieces = data.pieces;
|
||||
const list: CastRtfContent[] = [];
|
||||
for (let i = 0; i < pieces.length; i++) {
|
||||
if (!pieces[i].imageValue) {
|
||||
list.push({
|
||||
type: CastRtfContentType.TEXT,
|
||||
text: pieces[i].stringValue
|
||||
});
|
||||
} else {
|
||||
let url = pieces[i].imageValue?.image?.urlList?.[0];
|
||||
let name = pieces[i].imageValue?.image?.content?.name;
|
||||
list.push({
|
||||
type: CastRtfContentType.EMOJI,
|
||||
text: name,
|
||||
url
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收的二进制消息
|
||||
* @param data
|
||||
*/
|
||||
private async _decodeFrame(data: Uint8Array) {
|
||||
const frame = decodePushFrame(data);
|
||||
let payload = frame.payload;
|
||||
const headers = frame.headersList;
|
||||
let cursor = '';
|
||||
let internalExt = '';
|
||||
let needAck = !1;
|
||||
if (!payload) return null;
|
||||
if (headers) {
|
||||
if (headers['compress_type'] && headers['compress_type'] === 'gzip') {
|
||||
payload = pako.ungzip(payload);
|
||||
}
|
||||
if (headers['im-cursor']) {
|
||||
cursor = headers['im-cursor'];
|
||||
}
|
||||
if (headers['im-internal_ext']) {
|
||||
internalExt = headers['im-internal_ext'];
|
||||
}
|
||||
}
|
||||
const res = decodeResponse(payload);
|
||||
if (!cursor && res.cursor) cursor = res.cursor;
|
||||
if (!internalExt && res.internalExt) internalExt = res.internalExt;
|
||||
if (res.needAck) needAck = res.needAck;
|
||||
return {
|
||||
response: res,
|
||||
frame,
|
||||
cursor,
|
||||
needAck,
|
||||
internalExt
|
||||
};
|
||||
}
|
||||
|
||||
/** 心跳数据 */
|
||||
private _ping() {
|
||||
return encodePushFrame({
|
||||
payloadType: PayloadType.Hb
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ack 数据
|
||||
* @param ext Frame im-internal_ext | Response internalExt
|
||||
* @param logId
|
||||
*/
|
||||
private _ack(ext: string = '', logId?: string) {
|
||||
const getPayload = function (_ext: string) {
|
||||
let arr = [];
|
||||
for (let s of _ext) {
|
||||
let index = s.charCodeAt(0);
|
||||
index < 128
|
||||
? arr.push(index)
|
||||
: index < 2048
|
||||
? (arr.push(192 + (index >> 6)), arr.push(128 + (63 & index)))
|
||||
: index < 65536 &&
|
||||
(arr.push(224 + (index >> 12)), arr.push(128 + ((index >> 6) & 63)), arr.push(128 + (63 & index)));
|
||||
}
|
||||
return new Uint8Array(arr);
|
||||
};
|
||||
return encodePushFrame({
|
||||
payloadType: PayloadType.Ack,
|
||||
payload: getPayload(ext),
|
||||
logId
|
||||
});
|
||||
}
|
||||
|
||||
/** 关闭后 */
|
||||
private _afterClose() {
|
||||
this.state = !1;
|
||||
if (this.pingTimer) {
|
||||
clearTimeout(this.pingTimer);
|
||||
this.pingTimer = void 0;
|
||||
}
|
||||
this.cursor = {
|
||||
cursor: '',
|
||||
firstCursor: '',
|
||||
internalExt: ''
|
||||
};
|
||||
this.wsRoomStatus = WSRoomStatus.CLOSED;
|
||||
this.closeEvent = { code: DyCastCloseCode.NO_STATUS, msg: 'CLOSE_NO_STATUS' };
|
||||
this.ws = void 0;
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
/** 打开后 */
|
||||
private _afterOpen() {
|
||||
this.state = !0;
|
||||
this.wsRoomStatus = WSRoomStatus.CONNECTED;
|
||||
this.isReconnecting = false;
|
||||
this.reconnectCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的 wss 地址
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
private _getSocketUrl(opts: DyCastOptions) {
|
||||
const fullOpt = Object.assign({}, defaultOpts, opts);
|
||||
return `${BASE_URL}?${this._mergeOptions(fullOpt)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将配置转换为 url 参数字符串
|
||||
* - 如:item1=value1&item2=value2&...
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
private _mergeOptions(opts: any): string {
|
||||
return Object.keys(opts).reduce((t, n) => {
|
||||
let r;
|
||||
return `${t}${t ? '&' : ''}${n}=${null != (r = opts[n]) ? r : ''}`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
* @param roomNum
|
||||
* @returns
|
||||
*/
|
||||
private async fetchConnectInfo(roomNum: string) {
|
||||
try {
|
||||
const info = await getLiveInfo(roomNum);
|
||||
this.info = info;
|
||||
this.status = info.status;
|
||||
const res = await getImInfo(info.roomId, info.uniqueId);
|
||||
this.imInfo = res;
|
||||
} catch (err) {
|
||||
// CLog.error('DyCast LiveInfo Request Error =>', err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理连接参数对象
|
||||
*/
|
||||
private getWssParam(): DyCastOptions {
|
||||
const { roomId, uniqueId } = this.info;
|
||||
const sign = getSignature(roomId, uniqueId);
|
||||
return {
|
||||
room_id: roomId,
|
||||
user_unique_id: uniqueId,
|
||||
cursor: this.imInfo.cursor || '',
|
||||
internal_ext: this.imInfo.internalExt || '',
|
||||
signature: sign
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已经直播
|
||||
*/
|
||||
private isLiving() {
|
||||
return this.status === RoomStatus.LIVING;
|
||||
}
|
||||
|
||||
/** 获取直播状态 */
|
||||
private getLiveStatus() {
|
||||
let type = 'Unknown';
|
||||
let code = 0;
|
||||
let msg = '未知状态';
|
||||
switch (this.status) {
|
||||
case RoomStatus.PREPARE:
|
||||
type = 'PREPARE';
|
||||
code = RoomStatus.PREPARE;
|
||||
msg = '主播正在准备中';
|
||||
break;
|
||||
case RoomStatus.LIVING:
|
||||
type = 'LIVING';
|
||||
code = RoomStatus.LIVING;
|
||||
msg = '主播正在直播中';
|
||||
break;
|
||||
case RoomStatus.PAUSE:
|
||||
type = 'PAUSE';
|
||||
code = RoomStatus.PAUSE;
|
||||
msg = '主播暂时离开了';
|
||||
break;
|
||||
case RoomStatus.END:
|
||||
type = 'END';
|
||||
code = RoomStatus.END;
|
||||
msg = '主播已下播';
|
||||
break;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
code,
|
||||
msg
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export type EventMap = Record<string, (...args: any[]) => void>;
|
||||
|
||||
export class Emitter<T extends EventMap> {
|
||||
// 处理函数
|
||||
private listeners: {
|
||||
[K in keyof T]?: Set<T[K]>;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
* @param event 事件名称
|
||||
* @param handler 事件处理函数
|
||||
*/
|
||||
on<K extends keyof T>(event: K, handler: T[K]): void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = new Set();
|
||||
}
|
||||
this.listeners[event]!.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
* @param event 事件名称
|
||||
* @param handler 要移除的事件处理函数
|
||||
*/
|
||||
off<K extends keyof T>(event: K, handler: T[K]): void {
|
||||
const handlers = this.listeners[event];
|
||||
if (handlers) {
|
||||
handlers.delete(handler);
|
||||
if (handlers.size === 0) {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param event 事件名称
|
||||
* @param args 传递给处理函数的参数
|
||||
*/
|
||||
emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {
|
||||
const handlers = this.listeners[event];
|
||||
if (handlers) {
|
||||
// 创建副本防止在回调中添加/删除处理器影响当前循环
|
||||
const handlersCopy = new Set(handlers);
|
||||
handlersCopy.forEach(handler => {
|
||||
handler(...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性订阅
|
||||
* @param event 事件名称
|
||||
* @param handler 事件处理函数
|
||||
*/
|
||||
once<K extends keyof T>(event: K, handler: T[K]): void {
|
||||
const onceHandler = (...args: Parameters<T[K]>) => {
|
||||
this.off(event, onceHandler as T[K]);
|
||||
handler(...args);
|
||||
};
|
||||
this.on(event, onceHandler as T[K]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定事件的订阅数量
|
||||
* @param event 事件名称
|
||||
*/
|
||||
listenerCount<K extends keyof T>(event: K): number {
|
||||
return this.listeners[event]?.size || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有事件监听器或指定事件的所有监听器
|
||||
* @param event 可选,要清除的事件名称
|
||||
*/
|
||||
clear<K extends keyof T>(event?: K): void {
|
||||
if (event) {
|
||||
delete this.listeners[event];
|
||||
} else {
|
||||
this.listeners = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
-18
@@ -53,12 +53,20 @@ import ConnectInput from '@/components/ConnectInput.vue';
|
||||
import LiveInfo from '@/components/LiveInfo.vue';
|
||||
import LiveStatusPanel from '@/components/LiveStatusPanel.vue';
|
||||
import CastList from '@/components/CastList.vue';
|
||||
import { type ConnectStatus, type DyLiveInfo, type LiveRoom } from '@/core/dycast';
|
||||
import {
|
||||
CastMethod,
|
||||
DyCast,
|
||||
DyCastCloseCode,
|
||||
RoomStatus,
|
||||
type ConnectStatus,
|
||||
type DyLiveInfo,
|
||||
type DyMessage,
|
||||
type LiveRoom
|
||||
} from '@/core/dycast';
|
||||
import { verifyRoomNum, verifyWsUrl } from '@/utils/verifyUtil';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { getImInfo, getLiveInfo } from '@/core/request';
|
||||
import { getSignature } from '@/core/signature';
|
||||
import { CLog } from '@/utils/logUtil';
|
||||
import { getId } from '@/utils/idUtil';
|
||||
// 连接状态
|
||||
const connectStatus = ref<ConnectStatus>(0);
|
||||
// 转发状态
|
||||
@@ -87,6 +95,12 @@ const likeCount = ref<string | number>('*****');
|
||||
const castRef = useTemplateRef('castEl');
|
||||
// 其它弹幕
|
||||
const otherRef = useTemplateRef('otherEl');
|
||||
// 所有弹幕
|
||||
const allCasts: DyMessage[] = [];
|
||||
// 记录弹幕
|
||||
const castSet = new Set<string>();
|
||||
// 弹幕客户端
|
||||
let castWs: DyCast | undefined;
|
||||
|
||||
/**
|
||||
* 验证房间号
|
||||
@@ -137,32 +151,175 @@ const setRoomInfo = function (info?: DyLiveInfo) {
|
||||
if (info.avatar) avatar.value = info.avatar;
|
||||
if (info.nickname) nickname.value = info.nickname;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理消息列表
|
||||
*/
|
||||
const handleMessages = function (msgs: DyMessage[]) {
|
||||
const newCasts: DyMessage[] = [];
|
||||
const mainCasts: DyMessage[] = [];
|
||||
const otherCasts: DyMessage[] = [];
|
||||
try {
|
||||
for (const msg of msgs) {
|
||||
if (!msg.id) continue;
|
||||
if (castSet.has(msg.id)) continue;
|
||||
castSet.add(msg.id);
|
||||
switch (msg.method) {
|
||||
case CastMethod.CHAT:
|
||||
newCasts.push(msg);
|
||||
mainCasts.push(msg);
|
||||
break;
|
||||
case CastMethod.GIFT:
|
||||
if (!msg?.gift?.repeatEnd) {
|
||||
newCasts.push(msg);
|
||||
mainCasts.push(msg);
|
||||
}
|
||||
break;
|
||||
case CastMethod.LIKE:
|
||||
newCasts.push(msg);
|
||||
otherCasts.push(msg);
|
||||
setRoomCount(msg.room);
|
||||
break;
|
||||
case CastMethod.MEMBER:
|
||||
newCasts.push(msg);
|
||||
otherCasts.push(msg);
|
||||
setRoomCount(msg.room);
|
||||
break;
|
||||
case CastMethod.SOCIAL:
|
||||
newCasts.push(msg);
|
||||
otherCasts.push(msg);
|
||||
setRoomCount(msg.room);
|
||||
break;
|
||||
case CastMethod.EMOJI_CHAT:
|
||||
newCasts.push(msg);
|
||||
mainCasts.push(msg);
|
||||
break;
|
||||
case CastMethod.ROOM_USER_SEQ:
|
||||
setRoomCount(msg.room);
|
||||
break;
|
||||
case CastMethod.ROOM_STATS:
|
||||
setRoomCount(msg.room);
|
||||
break;
|
||||
case CastMethod.CONTROL:
|
||||
if (msg?.room?.status !== RoomStatus.LIVING) {
|
||||
// 已经下播
|
||||
newCasts.push(msg);
|
||||
otherCasts.push(msg);
|
||||
disconnectLive();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
// 记录
|
||||
allCasts.push(...newCasts);
|
||||
if (castRef.value) castRef.value.appendCasts(mainCasts);
|
||||
if (otherRef.value) otherRef.value.appendCasts(otherCasts);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加控制台消息
|
||||
* @param msg
|
||||
*/
|
||||
const addConsoleMessage = function (content: string) {
|
||||
if (otherRef.value)
|
||||
otherRef.value.appendCasts([
|
||||
{
|
||||
id: getId(),
|
||||
method: CastMethod.CUSTOM,
|
||||
content,
|
||||
user: { name: '控制台' }
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理列表
|
||||
*/
|
||||
function clearMessageList() {
|
||||
castSet.clear();
|
||||
allCasts.length = 0;
|
||||
if (castRef.value) castRef.value.clearCasts();
|
||||
if (otherRef.value) otherRef.value.clearCasts();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接房间
|
||||
*/
|
||||
const connectLive = function () {
|
||||
try {
|
||||
CLog.info(`正在连接房间:${roomNum.value}`);
|
||||
SkMessage.info(`正在连接房间:${roomNum.value}`);
|
||||
// 测试获取直播间连接信息
|
||||
getLiveInfo(roomNum.value)
|
||||
.then(info => {
|
||||
CLog.info('直播间连接信息1 =>', info);
|
||||
roomInputStatus.value = true;
|
||||
CLog.info('signature =>', getSignature(info.roomId, info.uniqueId));
|
||||
})
|
||||
.catch(err => {
|
||||
CLog.info('直播间连接信息获取失败');
|
||||
roomInputStatus.value = false;
|
||||
});
|
||||
// 清空上一次连接的消息
|
||||
clearMessageList();
|
||||
CLog.debug('正在连接:', roomNum.value);
|
||||
const cast = new DyCast(roomNum.value);
|
||||
cast.on('open', (ev, info) => {
|
||||
CLog.info('DyCast 房间连接成功');
|
||||
SkMessage.success(`房间连接成功[${roomNum.value}]`);
|
||||
roomInputStatus.value = true;
|
||||
connectStatus.value = 1;
|
||||
setRoomInfo(info);
|
||||
addConsoleMessage('直播间已连接');
|
||||
});
|
||||
cast.on('error', err => {
|
||||
CLog.error('DyCast 连接出错 =>', err);
|
||||
SkMessage.error(`连接出错: ${err}`);
|
||||
connectStatus.value = 2;
|
||||
roomInputStatus.value = false;
|
||||
});
|
||||
cast.on('close', (code, reason) => {
|
||||
CLog.info(`DyCast 房间已关闭[${code}] => ${reason}`);
|
||||
connectStatus.value = 3;
|
||||
roomInputStatus.value = false;
|
||||
switch (code) {
|
||||
case DyCastCloseCode.NORMAL:
|
||||
SkMessage.success('断开成功');
|
||||
break;
|
||||
case DyCastCloseCode.LIVE_END:
|
||||
SkMessage.info('主播已下播');
|
||||
break;
|
||||
case DyCastCloseCode.CANNOT_RECEIVE:
|
||||
SkMessage.error('无法正常接收信息,已关闭');
|
||||
break;
|
||||
default:
|
||||
SkMessage.info('房间已关闭');
|
||||
}
|
||||
if (code === DyCastCloseCode.LIVE_END) {
|
||||
addConsoleMessage(reason || '主播尚未开播或已下播');
|
||||
} else {
|
||||
if (statusPanelRef.value) addConsoleMessage(`连接已关闭,共持续: ${statusPanelRef.value.getDuration()}`);
|
||||
else addConsoleMessage('连接已关闭');
|
||||
}
|
||||
});
|
||||
cast.on('message', msgs => {
|
||||
handleMessages(msgs);
|
||||
});
|
||||
cast.on('reconnecting', (count, code, reason) => {
|
||||
switch (code) {
|
||||
case DyCastCloseCode.CANNOT_RECEIVE:
|
||||
// 无法正常接收信息
|
||||
SkMessage.warning('无法正常接收弹幕,准备重连中');
|
||||
break;
|
||||
default:
|
||||
CLog.warn('DyCast 重连中 =>', count);
|
||||
SkMessage.warning(`正在重连中: ${count}`);
|
||||
}
|
||||
});
|
||||
cast.on('reconnect', ev => {
|
||||
CLog.info('DyCast 重连成功');
|
||||
SkMessage.success('房间重连完成');
|
||||
});
|
||||
cast.connect();
|
||||
castWs = cast;
|
||||
} catch (err) {
|
||||
CLog.info('连接出错');
|
||||
CLog.error('房间连接过程出错:', err);
|
||||
SkMessage.error('房间连接过程出错');
|
||||
roomInputStatus.value = false;
|
||||
castWs = void 0;
|
||||
}
|
||||
};
|
||||
/** 断开连接 */
|
||||
const disconnectLive = function () {
|
||||
roomInputStatus.value = false;
|
||||
if (castWs) castWs.close(1000, '断开连接');
|
||||
};
|
||||
|
||||
/** 连接转发房间 */
|
||||
|
||||
Reference in new issue
Block a user