210 lines
5.9 KiB
JavaScript
210 lines
5.9 KiB
JavaScript
const MEDIA_HEADER_SIZE = 26;
|
||
const MEDIA_MAGIC = [0x43, 0x4d, 0x56, 0x46]; // CMVF
|
||
const MEDIA_MIME_TYPES = {
|
||
1: 'video/mp4; codecs="avc1.42C01E"'
|
||
};
|
||
|
||
class WebSocketManager {
|
||
constructor(url, userId) {
|
||
this.url = url;
|
||
this.userId = userId;
|
||
this.ws = null;
|
||
this.events = {};
|
||
this.reconnectTimer = null;
|
||
this.heartbeatTimer = null;
|
||
this.isManualClose = false;
|
||
this.messageQueue = [];
|
||
this.processingMessages = false;
|
||
this.maxQueueSize = 100;
|
||
this.status = false;
|
||
}
|
||
|
||
connect() {
|
||
if (!window.WebSocket) {
|
||
console.error('浏览器不支持 WebSocket');
|
||
return;
|
||
}
|
||
|
||
this.ws = new WebSocket(`${this.url}?userId=${this.userId}`, [], { perMessageDeflate: true });
|
||
this.ws.binaryType = 'arraybuffer'; // 确保接收 ArrayBuffer
|
||
|
||
this.ws.onopen = () => {
|
||
console.log('WebSocket 已连接');
|
||
this.status = true;
|
||
this.startHeartbeat();
|
||
this.processMessageQueue();
|
||
this.dispatch('open');
|
||
};
|
||
|
||
this.ws.onmessage = async (event) => {
|
||
const data = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
|
||
if (data instanceof ArrayBuffer && this.isMediaPacket(data)) {
|
||
this.dispatchMediaPacket(data);
|
||
return;
|
||
}
|
||
if (this.messageQueue.length >= this.maxQueueSize) {
|
||
console.warn('WebSocket控制消息队列已满,丢弃最旧消息');
|
||
this.messageQueue.shift();
|
||
}
|
||
this.messageQueue.push(data);
|
||
this.processMessageQueue();
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket 断开');
|
||
this.status = false;
|
||
this.stopHeartbeat();
|
||
this.dispatch('close');
|
||
if (!this.isManualClose) {
|
||
this.reconnect();
|
||
}
|
||
};
|
||
|
||
this.ws.onerror = (err) => {
|
||
console.error('WebSocket 错误:', err);
|
||
};
|
||
}
|
||
|
||
isMediaPacket(buffer) {
|
||
if (buffer.byteLength < MEDIA_HEADER_SIZE) return false;
|
||
const bytes = new Uint8Array(buffer, 0, MEDIA_MAGIC.length);
|
||
return MEDIA_MAGIC.every((value, index) => bytes[index] === value);
|
||
}
|
||
|
||
dispatchMediaPacket(buffer) {
|
||
try {
|
||
const view = new DataView(buffer);
|
||
const version = view.getUint8(4);
|
||
const mediaType = view.getUint8(5);
|
||
const timestamp = view.getUint32(8) * 0x100000000 + view.getUint32(12);
|
||
const sequence = view.getUint32(16);
|
||
const channelLength = view.getUint16(20);
|
||
const payloadLength = view.getUint32(22);
|
||
const payloadOffset = MEDIA_HEADER_SIZE + channelLength;
|
||
if (version !== 1 || payloadOffset + payloadLength !== buffer.byteLength) {
|
||
throw new Error('媒体数据包头无效');
|
||
}
|
||
const channel = new TextDecoder().decode(new Uint8Array(buffer, MEDIA_HEADER_SIZE, channelLength));
|
||
const mimeType = MEDIA_MIME_TYPES[mediaType];
|
||
if (!mimeType) {
|
||
throw new Error(`不支持的媒体类型: ${mediaType}`);
|
||
}
|
||
this.dispatch(channel, {
|
||
media: true,
|
||
mimeType,
|
||
sequence,
|
||
timestamp,
|
||
data: buffer.slice(payloadOffset, payloadOffset + payloadLength)
|
||
});
|
||
} catch (error) {
|
||
console.error('WebSocket媒体包解析失败:', error);
|
||
}
|
||
}
|
||
|
||
async processMessageQueue() {
|
||
if (this.processingMessages || !this.ws) return;
|
||
this.processingMessages = true;
|
||
try {
|
||
while (this.messageQueue.length > 0) {
|
||
const data = this.messageQueue.shift();
|
||
let message;
|
||
if (typeof data === 'string') {
|
||
message = JSON.parse(data);
|
||
} else if (data instanceof ArrayBuffer) {
|
||
const decompressedStream = new Blob([data])
|
||
.stream()
|
||
.pipeThrough(new DecompressionStream('gzip'));
|
||
const decompressedData = await new Response(decompressedStream).arrayBuffer();
|
||
message = JSON.parse(new TextDecoder().decode(decompressedData));
|
||
} else {
|
||
throw new Error(`不支持的WebSocket数据类型: ${typeof data}`);
|
||
}
|
||
if (message.channel) {
|
||
this.dispatch(message.channel, message.payload);
|
||
} else if (message.type) {
|
||
this.dispatch(message.type, message.payload ?? message.content ?? message);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('WebSocket消息解析失败:', err);
|
||
} finally {
|
||
this.processingMessages = false;
|
||
if (this.messageQueue.length > 0) {
|
||
queueMicrotask(() => this.processMessageQueue());
|
||
}
|
||
}
|
||
}
|
||
|
||
dispatch(eventName, payload) {
|
||
const callbacks = this.events[eventName];
|
||
if (callbacks) {
|
||
callbacks.slice().forEach(callback => callback(payload));
|
||
}
|
||
}
|
||
|
||
on(eventName, callback) {
|
||
if (!this.events[eventName]) {
|
||
this.events[eventName] = [];
|
||
}
|
||
this.events[eventName].push(callback);
|
||
}
|
||
|
||
off(eventName, callback) {
|
||
if (this.events[eventName]) {
|
||
const index = this.events[eventName].indexOf(callback);
|
||
if (index !== -1) {
|
||
this.events[eventName].splice(index, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
send(payload) {
|
||
if (this.ws && this.ws.readyState === 1) {
|
||
this.ws.send(JSON.stringify(payload));
|
||
} else {
|
||
console.warn('WebSocket 未连接,消息发送失败');
|
||
}
|
||
}
|
||
|
||
close() {
|
||
this.isManualClose = true;
|
||
this.status = false;
|
||
this.stopHeartbeat();
|
||
if (this.ws) {
|
||
this.ws.close();
|
||
}
|
||
}
|
||
|
||
reconnect() {
|
||
clearTimeout(this.reconnectTimer);
|
||
this.reconnectTimer = setTimeout(() => {
|
||
console.log('尝试重新连接 WebSocket...');
|
||
this.connect();
|
||
}, 5000);
|
||
}
|
||
|
||
startHeartbeat() {
|
||
this.heartbeatTimer = setInterval(() => {
|
||
this.send({ type: 'heartbeat' });
|
||
}, 30000);
|
||
}
|
||
|
||
stopHeartbeat() {
|
||
clearInterval(this.heartbeatTimer);
|
||
}
|
||
|
||
status() {
|
||
return this.status;
|
||
}
|
||
}
|
||
|
||
export default {
|
||
install(app, options) {
|
||
const { url, userId } = options;
|
||
const wsManager = new WebSocketManager(url, userId);
|
||
wsManager.connect();
|
||
app.config.globalProperties.$ws = wsManager;
|
||
app.provide('ws', wsManager);
|
||
}
|
||
};
|