UNPKG

yirtc-sdk-web

Version:

基于WebRTC的实时音视频SDK

1,431 lines 60.7 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
/**
 * YiRTC SDK 客户端类
 */
import { EventEmitter } from './utils/eventEmitter';
import { Logger } from './utils/logger';
import { ConnectionState, ClientRole, ChannelProfile, ErrorCode, MediaTypeEnum } from './types';
// 导入socket.io-client
import * as SocketIO from 'socket.io-client';
/**
 * 客户端类,负责管理房间连接和流发布/订阅
 */
export class Client extends EventEmitter {
    /**
     * 创建客户端实例
     * @param config 客户端配置
     */
    constructor(config) {
        super();
        this.connectionState = ConnectionState.DISCONNECTED;
        this.channelName = '';
        this.uid = '';
        this.role = ClientRole.HOST;
        this.channelProfile = ChannelProfile.COMMUNICATION;
        this.localStream = null;
        this.remoteStreams = new Map();
        this.peerConnections = new Map();
        this.signalConnection = null; // Socket.io实例
        this.config = config;
        this.logger = new Logger('Client');
        this.logger.info('客户端已创建', { appKey: config.appKey });
    }
    /**
     * 加入频道
     * @param options 加入选项
     * @returns Promise<void>
     */
    join(options) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connectionState === ConnectionState.CONNECTED) {
                this.logger.warn('已经连接到频道,请先离开当前频道');
                throw new Error('已经连接到频道');
            }
            try {
                this.channelName = options.channelName;
                this.uid = options.uid;
                // 更新连接状态
                this._updateConnectionState(ConnectionState.CONNECTING);
                // 创建信令连接
                yield this._createSignalConnection(options);
                this._updateConnectionState(ConnectionState.CONNECTED);
                this.emit('joined', { uid: this.uid, channelName: this.channelName });
                this.logger.info('成功加入频道', { channelName: this.channelName, uid: this.uid });
                return;
            }
            catch (error) {
                this._updateConnectionState(ConnectionState.FAILED);
                this.logger.error('加入频道失败', error);
                this.emit('error', { code: ErrorCode.SIGNALING_JOIN_FAILED, message: '加入频道失败', error });
                throw error;
            }
        });
    }
    /**
     * 离开频道
     * @returns Promise<void>
     */
    leave() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connectionState !== ConnectionState.CONNECTED) {
                this.logger.warn('当前未连接到任何频道');
                return;
            }
            try {
                // 关闭所有对等连接
                this.peerConnections.forEach((pc) => {
                    pc.close();
                });
                this.peerConnections.clear();
                // 关闭信令连接
                if (this.signalConnection) {
                    this.signalConnection.close();
                    this.signalConnection = null;
                }
                // 清理远程流
                this.remoteStreams.clear();
                // 更新连接状态
                this._updateConnectionState(ConnectionState.DISCONNECTED);
                const channelName = this.channelName;
                const uid = this.uid;
                this.channelName = '';
                this.uid = '';
                this.emit('left', { channelName, uid });
                this.logger.info('已离开频道', { channelName, uid });
            }
            catch (error) {
                this.logger.error('离开频道失败', error);
                throw error;
            }
        });
    }
    /**
     * 发布本地流
     * @param stream 本地流实例
     * @param mediaType 媒体类型,不填写则默认为 'all'
     * @returns Promise<void>
     */
    publish(stream, mediaType) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connectionState !== ConnectionState.CONNECTED) {
                this.logger.error('未连接到频道,无法发布流');
                throw new Error('未连接到频道');
            }
            if (this.localStream) {
                this.logger.warn('已有本地流发布,请先取消发布');
                throw new Error('已有本地流发布');
            }
            try {
                this.localStream = stream;
                // 处理mediaType参数,默认为'all'
                const effectiveMediaType = mediaType || 'all';
                this.logger.info('发布流的媒体类型为:' + effectiveMediaType);
                // 在实际应用中,这里需要创建与每个远程用户的对等连接并发送offer
                // 简化版中,我们发送流发布消息给房间内的其他用户
                // 【平台适配注意事项】
                // 如果要适配其他平台(如网易云信、声网、腾讯云等),需要修改以下部分:
                // 1. 信令消息格式:不同平台的消息结构和字段名可能不同
                // 2. 消息发送方式:有些平台使用WebSocket原生send,有些使用Socket.io的emit
                // 3. 消息类型定义:STREAM_PUBLISHED等类型名称可能需要调整
                // 4. 房间和用户标识:roomId、from、to等字段的命名和格式可能不同
                // 5. 流信息字段:hasVideo、hasAudio等媒体信息的表示方式可能不同
                if (this.signalConnection) {
                    const message = {
                        type: 'STREAM_PUBLISHED',
                        from: this.uid,
                        to: 'all',
                        roomId: this.channelName,
                        streamId: stream.getId(),
                        hasVideo: stream.hasVideo(),
                        hasAudio: stream.hasAudio(),
                        hasScreen: stream.isScreenStream(),
                        mediaType: stream.getMediaType(),
                        publishMediaType: effectiveMediaType // 发布时指定的媒体类型
                    };
                    this.logger.info('准备发送流发布消息', {
                        message,
                        socketConnected: this.signalConnection.connected,
                        socketId: this.signalConnection.id
                    });
                    this.signalConnection.emit('message', message);
                    this.logger.info('流发布消息已发送', { streamId: stream.getId(), roomId: this.channelName });
                }
                else {
                    this.logger.error('信令连接不存在,无法发送消息');
                }
                this.emit('stream-published', { stream });
                this.logger.info('本地流发布成功', { streamId: stream.getId() });
            }
            catch (error) {
                this.logger.error('发布本地流失败', error);
                throw error;
            }
        });
    }
    /**
     * 取消发布本地流
     * @returns Promise<void>
     */
    unpublish() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.localStream) {
                this.logger.warn('没有发布的本地流');
                return;
            }
            try {
                const stream = this.localStream;
                this.localStream = null;
                // 发送取消发布信令
                if (this.signalConnection) {
                    this.signalConnection.emit('message', {
                        type: 'STREAM_UNPUBLISHED',
                        from: this.uid,
                        to: 'all',
                        roomId: this.channelName,
                        streamId: stream.getId()
                    });
                    this.logger.info('发送流取消发布消息', { streamId: stream.getId(), roomId: this.channelName });
                }
                this.emit('stream-unpublished', { stream });
                this.logger.info('取消发布本地流成功', { streamId: stream.getId() });
            }
            catch (error) {
                this.logger.error('取消发布本地流失败', error);
                throw error;
            }
        });
    }
    /**
     * 订阅远程流
     * @param remoteUserId 远程用户ID
     * @param streamId 流ID
     * @param options 订阅选项,可选
     * @returns Promise<void>
     */
    subscribe(remoteUserId, streamId, options) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connectionState !== ConnectionState.CONNECTED) {
                this.logger.error('未连接到频道,无法订阅流');
                throw new Error('未连接到频道');
            }
            try {
                this.logger.info('开始订阅远程流', { remoteUserId, streamId, options });
                // 创建 RTCPeerConnection
                const peerConnection = yield this._createPeerConnection(remoteUserId);
                // 根据订阅选项设置音视频接收
                const offerToReceiveAudio = (options === null || options === void 0 ? void 0 : options.audio) !== false; // 默认接收音频
                const offerToReceiveVideo = (options === null || options === void 0 ? void 0 : options.video) !== false; // 默认接收视频
                // 发送 offer 信令 - 优化低延迟
                const offer = yield peerConnection.createOffer({
                    offerToReceiveAudio,
                    offerToReceiveVideo,
                    // 优化配置降低延迟
                    iceRestart: false // 不重启ICE,减少连接时间
                });
                yield peerConnection.setLocalDescription(offer);
                // 设置H.264编解码器偏好(在设置本地描述后)
                this._setH264CodecPreferences(peerConnection);
                // 如果需要使用SDP修改方式,则修改offer
                let finalOffer = offer;
                if (peerConnection._needsH264SdpModification) {
                    finalOffer = this._forceH264CodecViaSDP(offer);
                }
                // 发送 offer 给远程用户
                if (this.signalConnection) {
                    this.signalConnection.emit('message', {
                        type: 'OFFER',
                        from: this.uid,
                        to: remoteUserId,
                        roomId: this.channelName,
                        streamId: streamId,
                        offer: finalOffer
                    });
                }
                this.logger.info('已发送 offer 信令', { remoteUserId, streamId });
            }
            catch (error) {
                this.logger.error('订阅远程流失败', error);
                throw error;
            }
        });
    }
    /**
     * 取消订阅远程流
     * @param stream 远程流实例
     * @param options 取消订阅选项,可选
     * @returns Promise<void>
     */
    unsubscribe(stream, options) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const remoteUid = stream.getUserId();
                if (!this.remoteStreams.has(remoteUid)) {
                    this.logger.warn('未订阅该远程流');
                    return;
                }
                // 发送取消订阅信令
                if (this.signalConnection) {
                    this.signalConnection.send(JSON.stringify({
                        type: 'unsubscribe',
                        uid: this.uid,
                        remoteUid,
                        streamId: stream.getId()
                    }));
                }
                this.remoteStreams.delete(remoteUid);
                this.emit('stream-unsubscribed', { stream });
                this.logger.info('取消订阅远程流成功', { streamId: stream.getId(), remoteUid });
            }
            catch (error) {
                this.logger.error('取消订阅远程流失败', error);
                throw error;
            }
        });
    }
    /**
     * 设置频道模式
     * @param profile 频道模式
     */
    setChannelProfile(profile) {
        this.channelProfile = profile;
        this.logger.info('设置频道模式', { profile });
    }
    /**
     * 设置客户端角色
     * @param role 客户端角色
     */
    setClientRole(role) {
        this.role = role;
        this.logger.info('设置客户端角色', { role });
        this.emit('client-role-changed', { role });
    }
    /**
     * 获取连接状态
     * @returns ConnectionState
     */
    getConnectionState() {
        return this.connectionState;
    }
    /**
     * 获取用户ID
     * @returns string | number
     */
    getUid() {
        return this.uid;
    }
    /**
     * 销毁客户端实例
     */
    destroy() {
        if (this.connectionState === ConnectionState.CONNECTED) {
            this.leave();
        }
        this.removeAllListeners();
        this.logger.info('客户端已销毁');
    }
    /**
     * 获取本地音频统计信息
     * @returns LocalAudioStats
     */
    getLocalAudioStats() {
        // 在实际应用中,这里应该从WebRTC获取真实数据
        return {
            sendBitrate: 0,
            sendVolume: 0
        };
    }
    /**
     * 获取本地视频统计信息
     * @returns LocalVideoStats
     */
    getLocalVideoStats() {
        // 在实际应用中,这里应该从WebRTC获取真实数据
        return {
            sendBitrate: 0,
            sendFrameRate: 0,
            sendResolution: {
                width: 0,
                height: 0
            }
        };
    }
    /**
     * 获取远程音频统计信息
     * @returns Record<string, RemoteAudioStats>
     */
    getRemoteAudioStats() {
        // 在实际应用中,这里应该从WebRTC获取真实数据
        const stats = {};
        this.remoteStreams.forEach((stream, uid) => {
            stats[String(uid)] = {
                uid,
                receiveBitrate: 0,
                receiveVolume: 0,
                audioLossRate: 0
            };
        });
        return stats;
    }
    /**
     * 获取远程视频统计信息
     * @returns Record<string, RemoteVideoStats>
     */
    getRemoteVideoStats() {
        // 在实际应用中,这里应该从WebRTC获取真实数据
        const stats = {};
        this.remoteStreams.forEach((stream, uid) => {
            stats[String(uid)] = {
                uid,
                receiveBitrate: 0,
                receiveFrameRate: 0,
                receiveResolution: {
                    width: 0,
                    height: 0
                }
            };
        });
        return stats;
    }
    /**
     * 获取网络统计信息
     * @returns NetworkStats
     */
    getNetworkStats() {
        // 在实际应用中,这里应该从WebRTC获取真实数据
        return {
            uplinkNetworkQuality: 0,
            downlinkNetworkQuality: 0
        };
    }
    /**
     * 设置远程视频流类型(大小流)
     * @param uid 远程用户ID
     * @param streamType 流类型(0: 大流, 1: 小流)
     */
    setRemoteVideoStreamType(uid, streamType) {
        this.logger.info('设置远程视频流类型', { uid, streamType });
        // 在实际应用中,这里需要发送信令来切换大小流
    }
    /**
     * 启用双流模式
     */
    enableDualStream() {
        this.logger.info('启用双流模式');
        // 在实际应用中,这里需要配置WebRTC启用模拟大小流
    }
    /**
     * 禁用双流模式
     */
    disableDualStream() {
        this.logger.info('禁用双流模式');
        // 在实际应用中,这里需要配置WebRTC禁用模拟大小流
    }
    /**
     * 设置播放音量
     * @param volume 音量值 (0-100)
     */
    setPlaybackVolume(volume) {
        if (volume < 0 || volume > 100) {
            this.logger.warn('音量值必须在0-100之间');
            return;
        }
        this.logger.info('设置播放音量', { volume });
        // 在实际应用中,这里需要设置音频元素的音量
    }
    /**
     * 创建 WebRTC 对等连接
     * @param remoteUserId 远程用户ID
     * @returns Promise<RTCPeerConnection>
     */
    _createPeerConnection(remoteUserId) {
        return __awaiter(this, void 0, void 0, function* () {
            // 如果已经存在连接,直接返回
            if (this.peerConnections.has(remoteUserId)) {
                return this.peerConnections.get(remoteUserId);
            }
            // WebRTC 配置 - 优化低延迟
            const rtcConfig = {
                iceServers: [
                    { urls: 'stun:stun.l.google.com:19302' },
                    { urls: 'stun:stun1.l.google.com:19302' },
                    { urls: 'stun:stun2.l.google.com:19302' },
                    { urls: 'stun:stun3.l.google.com:19302' },
                    // 添加更多STUN服务器提高连接成功率
                    { urls: 'stun:stun.stunprotocol.org:3478' },
                    { urls: 'stun:stun.voiparound.com' },
                    { urls: 'stun:stun.voipbuster.com' }
                ],
                iceCandidatePoolSize: 10,
                // 优化配置降低延迟
                bundlePolicy: 'max-bundle',
                rtcpMuxPolicy: 'require',
                iceTransportPolicy: 'all' // 允许所有ICE传输
            };
            const peerConnection = new RTCPeerConnection(rtcConfig);
            this.peerConnections.set(remoteUserId, peerConnection);
            // 设置事件监听器
            peerConnection.onicecandidate = (event) => {
                if (event.candidate) {
                    // 优化:优先发送本地候选,延迟发送其他类型候选
                    const candidate = event.candidate;
                    const candidateStr = candidate.candidate;
                    if (candidateStr.includes('typ host')) {
                        // 本地候选立即发送
                        if (this.signalConnection) {
                            this.signalConnection.emit('message', {
                                type: 'CANDIDATE',
                                from: this.uid,
                                to: remoteUserId,
                                roomId: this.channelName,
                                candidate: candidate
                            });
                            this.logger.debug('发送本地 ICE candidate', { remoteUserId });
                        }
                    }
                    else {
                        // 其他候选延迟发送,给本地候选优先机会
                        setTimeout(() => {
                            if (this.signalConnection) {
                                this.signalConnection.emit('message', {
                                    type: 'CANDIDATE',
                                    from: this.uid,
                                    to: remoteUserId,
                                    roomId: this.channelName,
                                    candidate: candidate
                                });
                                this.logger.debug('发送非本地 ICE candidate', { remoteUserId, type: candidate.type });
                            }
                        }, 100);
                    }
                    // 全面检测ICE候选相关的延迟问题
                    this._analyzeICECandidate(candidate);
                }
            };
            peerConnection.ontrack = (event) => {
                this.logger.info('收到远程流', { remoteUserId, streams: event.streams.length });
                if (event.streams && event.streams[0]) {
                    const remoteStream = event.streams[0];
                    // 触发远程流订阅成功事件
                    this.emit('stream-subscribed', {
                        userId: remoteUserId,
                        stream: remoteStream
                    });
                }
            };
            peerConnection.onconnectionstatechange = () => {
                this.logger.info('Peer connection 状态变化', {
                    remoteUserId,
                    state: peerConnection.connectionState
                });
                // 当连接成功时,开始监控统计信息
                if (peerConnection.connectionState === 'connected') {
                    this._startStatsMonitoring(remoteUserId, peerConnection);
                }
            };
            // 监含ICE连接状态变化
            peerConnection.oniceconnectionstatechange = () => {
                this.logger.info('ICE 连接状态变化', {
                    remoteUserId,
                    iceState: peerConnection.iceConnectionState
                });
            };
            // 添加本地流的轨道到peer connection,并设置H.264编解码器偏好
            if (this.localStream) {
                const mediaStream = this.localStream.getMediaStream();
                if (mediaStream) {
                    this._addStreamWithH264Preference(peerConnection, mediaStream);
                }
            }
            return peerConnection;
        });
    }
    /**
     * 清理指定用户的WebRTC连接
     * @param remoteUserId 远程用户ID
     */
    _cleanupPeerConnection(remoteUserId) {
        const peerConnection = this.peerConnections.get(remoteUserId);
        if (peerConnection) {
            this.logger.info('🧹 清理WebRTC连接', { remoteUserId, state: peerConnection.connectionState });
            try {
                // 关闭连接
                peerConnection.close();
                // 从映射中移除
                this.peerConnections.delete(remoteUserId);
                // 清理远程流
                this.remoteStreams.delete(remoteUserId);
                this.logger.info('✅ WebRTC连接清理完成', { remoteUserId });
            }
            catch (error) {
                this.logger.error('❌ 清理WebRTC连接失败', { remoteUserId, error });
            }
        }
        else {
            this.logger.debug('🔍 未找到需要清理的WebRTC连接', { remoteUserId });
        }
    }
    /**
     * 创建信令连接
     * @param options 加入选项
     * @returns Promise<void>
     */
    _createSignalConnection(options) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                try {
                    // 确保房间号和用户ID都已正确设置
                    if (!options.channelName) {
                        throw new Error('房间号不能为空');
                    }
                    if (!options.uid) {
                        throw new Error('用户ID不能为空');
                    }
                    // 直接使用传入的房间号,不做任何前缀处理
                    this.channelName = options.channelName;
                    this.uid = options.uid;
                    this.logger.info('创建信令连接', {
                        channelName: this.channelName,
                        uid: this.uid
                    });
                    // 真正创建Socket.io连接
                    this._createSocketIOConnection(resolve, reject);
                }
                catch (error) {
                    this.logger.error('创建信令连接失败', error);
                    reject(error);
                }
            });
        });
    }
    /**
     * 创建Socket.io连接
     * @param resolve Promise resolve回调
     * @param reject Promise reject回调
     */
    _createSocketIOConnection(resolve, reject) {
        try {
            // 使用静态导入的socket.io-client
            const io = SocketIO.default || SocketIO;
            // 信令服务器配置
            const signalingUrl = 'https://172.19.11.228:8444';
            const socketOptions = {
                reconnection: true,
                reconnectionAttempts: 5,
                reconnectionDelay: 3000,
                timeout: 10000,
                rejectUnauthorized: false,
                transports: ['websocket', 'polling']
            };
            this.logger.info('开始连接Socket.io服务器', { url: signalingUrl });
            // 创建socket连接
            const socket = io(signalingUrl, socketOptions);
            // 连接成功
            socket.on('connect', () => {
                this.logger.info('Socket.io连接成功', { socketId: socket.id });
                // 保存socket连接
                this.signalConnection = socket;
                // 监听服务器消息
                this._setupSocketListeners(socket);
                // 加入房间
                this._joinRoom(socket);
                resolve();
            });
            // 连接失败
            socket.on('connect_error', (error) => {
                this.logger.error('Socket.io连接失败', error);
                reject(new Error(`Socket.io连接失败: ${error.message}`));
            });
            // 连接断开
            socket.on('disconnect', (reason) => {
                this.logger.warn('Socket.io连接断开', { reason });
                this._updateConnectionState(ConnectionState.DISCONNECTED);
            });
        }
        catch (error) {
            this.logger.error('创建Socket.io连接异常', error);
            reject(error);
        }
    }
    /**
     * 设置Socket监听器
     * @param socket Socket.io实例
     */
    _setupSocketListeners(socket) {
        // 监听clientId事件
        socket.on('clientId', (data) => {
            this.logger.info('收到clientId', data);
        });
        // 监听message事件
        socket.on('message', (message) => {
            this.logger.info('收到服务器消息', message);
            // 处理流发布消息
            if (message.type === 'STREAM_PUBLISHED' && message.from !== this.uid) {
                this.logger.info('📡 检测到远程流发布', { from: message.from, streamId: message.streamId });
                // 🔧 关键修复:如果是流重新发布,先清理旧连接
                if (this.peerConnections.has(message.from)) {
                    this.logger.info('🔄 检测到流重新发布,清理旧连接', { from: message.from });
                    this._cleanupPeerConnection(message.from);
                }
                // 创建远程流对象,符合RemoteStreamInfo接口
                const hasVideo = message.hasVideo || true;
                const hasAudio = message.hasAudio || true;
                const hasScreen = message.hasScreen || false;
                // 根据流特征推断MediaType
                let mediaType = MediaTypeEnum.VIDEO; // 默认为视频
                if (hasScreen) {
                    mediaType = MediaTypeEnum.SCREEN;
                }
                else if (hasAudio && !hasVideo) {
                    mediaType = MediaTypeEnum.AUDIO;
                }
                const remoteStreamInfo = {
                    streamId: message.streamId,
                    userId: message.from,
                    hasVideo,
                    hasAudio,
                    hasScreen,
                    mediaType
                };
                // 触发stream-added事件
                this.emit('stream-added', remoteStreamInfo);
            }
            // 处理流取消发布消息
            if (message.type === 'STREAM_UNPUBLISHED' && message.from !== this.uid) {
                this.logger.info('检测到远程流取消发布', { from: message.from, streamId: message.streamId });
                const remoteStreamInfo = {
                    streamId: message.streamId,
                    userId: message.from,
                    hasVideo: message.hasVideo,
                    hasAudio: message.hasAudio,
                    hasScreen: message.hasScreen,
                    mediaType: message.mediaType
                };
                // 🔧 关键修复:清理对应的WebRTC连接
                this._cleanupPeerConnection(message.from);
                // 触发stream-removed事件
                this.emit('stream-removed', remoteStreamInfo);
            }
            // 处理 WebRTC 信令消息
            this._handleWebRTCSignaling(message);
            // 转发消息给上层应用
            this.emit('message', message);
        });
        // 监听错误事件
        socket.on('error', (error) => {
            this.logger.error('Socket.io错误', error);
            this.emit('error', { code: ErrorCode.SIGNALING_CONNECTION_ERROR, message: 'Socket.io错误', error });
        });
    }
    /**
     * 处理 WebRTC 信令消息
     * @param message 信令消息
     */
    _handleWebRTCSignaling(message) {
        return __awaiter(this, void 0, void 0, function* () {
            if (message.to !== this.uid) {
                return; // 不是发给我的消息
            }
            const remoteUserId = message.from;
            try {
                switch (message.type) {
                    case 'OFFER':
                        yield this._handleOffer(remoteUserId, message.offer);
                        break;
                    case 'ANSWER':
                        yield this._handleAnswer(remoteUserId, message.answer);
                        break;
                    case 'CANDIDATE':
                        yield this._handleCandidate(remoteUserId, message.candidate);
                        break;
                }
            }
            catch (error) {
                this.logger.error('WebRTC 信令处理失败', { type: message.type, remoteUserId, error });
            }
        });
    }
    /**
     * 处理 Offer 信令
     * @param remoteUserId 远程用户ID
     * @param offer Offer 对象
     */
    _handleOffer(remoteUserId, offer) {
        return __awaiter(this, void 0, void 0, function* () {
            this.logger.info('收到 Offer 信令', { remoteUserId });
            // 创建或获取 peer connection
            const peerConnection = yield this._createPeerConnection(remoteUserId);
            // 先设置H.264编解码器偏好
            this._setH264CodecPreferences(peerConnection);
            // 如果需要,修改接收到的offer的SDP
            let modifiedOffer = offer;
            if (peerConnection._needsH264SdpModification) {
                modifiedOffer = this._forceH264CodecViaSDP(offer);
            }
            // 设置远程描述
            yield peerConnection.setRemoteDescription(new RTCSessionDescription(modifiedOffer));
            // 创建 answer
            const answer = yield peerConnection.createAnswer();
            yield peerConnection.setLocalDescription(answer);
            // 如果需要,修改answer的SDP
            let finalAnswer = answer;
            if (peerConnection._needsH264SdpModification) {
                finalAnswer = this._forceH264CodecViaSDP(answer);
            }
            // 发送 answer
            if (this.signalConnection) {
                this.signalConnection.emit('message', {
                    type: 'ANSWER',
                    from: this.uid,
                    to: remoteUserId,
                    roomId: this.channelName,
                    answer: finalAnswer
                });
            }
            this.logger.info('已发送 Answer 信令', { remoteUserId });
        });
    }
    /**
     * 处理 Answer 信令
     * @param remoteUserId 远程用户ID
     * @param answer Answer 对象
     */
    _handleAnswer(remoteUserId, answer) {
        return __awaiter(this, void 0, void 0, function* () {
            this.logger.info('收到 Answer 信令', { remoteUserId });
            const peerConnection = this.peerConnections.get(remoteUserId);
            if (peerConnection) {
                // 如果需要,修改接收到的answer的SDP
                let modifiedAnswer = answer;
                if (peerConnection._needsH264SdpModification) {
                    modifiedAnswer = this._forceH264CodecViaSDP(answer);
                }
                yield peerConnection.setRemoteDescription(new RTCSessionDescription(modifiedAnswer));
                this.logger.info('已设置远程描述', { remoteUserId });
            }
        });
    }
    /**
     * 处理 ICE Candidate 信令
     * @param remoteUserId 远程用户ID
     * @param candidate ICE Candidate 对象
     */
    _handleCandidate(remoteUserId, candidate) {
        return __awaiter(this, void 0, void 0, function* () {
            this.logger.debug('收到 ICE Candidate', { remoteUserId });
            const peerConnection = this.peerConnections.get(remoteUserId);
            if (peerConnection) {
                yield peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
            }
        });
    }
    /**
     * 加入房间
     * @param socket Socket.io实例
     */
    _joinRoom(socket) {
        const joinMessage = {
            type: 'JOIN',
            from: this.uid,
            to: 'server',
            roomId: this.channelName,
            userId: this.uid
        };
        this.logger.info('发送加入房间消息', joinMessage);
        socket.emit('message', joinMessage);
    }
    /**
     * 更新连接状态
     * @param state 新状态
     */
    _updateConnectionState(state) {
        if (this.connectionState === state)
            return;
        const oldState = this.connectionState;
        this.connectionState = state;
        this.emit('connection-state-change', {
            current: state,
            previous: oldState
        });
        this.logger.info('连接状态变更', { from: oldState, to: state });
    }
    /**
     * 开始监控WebRTC统计信息
     * @param remoteUserId 远程用户ID
     * @param peerConnection RTCPeerConnection实例
     */
    _startStatsMonitoring(remoteUserId, peerConnection) {
        // 每5秒收集一次统计信息
        const statsInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
            try {
                if (peerConnection.connectionState === 'closed' || peerConnection.connectionState === 'failed') {
                    clearInterval(statsInterval);
                    return;
                }
                const stats = yield peerConnection.getStats();
                this._processWebRTCStats(remoteUserId, stats);
            }
            catch (error) {
                this.logger.error('获取WebRTC统计信息失败', { remoteUserId, error });
            }
        }), 5000);
        // 当连接关闭时清理定时器
        peerConnection.addEventListener('connectionstatechange', () => {
            if (peerConnection.connectionState === 'closed' || peerConnection.connectionState === 'failed') {
                clearInterval(statsInterval);
            }
        });
    }
    /**
     * 处理WebRTC统计信息
     * @param remoteUserId 远程用户ID
     * @param stats RTCStatsReport
     */
    _processWebRTCStats(remoteUserId, stats) {
        let videoPacketsLost = 0;
        let videoPacketsReceived = 0;
        let audioPacketsLost = 0;
        let audioPacketsReceived = 0;
        let currentRTT = 0;
        let connectionType = 'unknown';
        let candidateType = 'unknown';
        let jitter = 0;
        let bandwidth = 0;
        let codecName = '';
        let frameRate = 0;
        let resolution = '';
        stats.forEach((report) => {
            if (report.type === 'inbound-rtp') {
                if (report.kind === 'video') {
                    videoPacketsLost += report.packetsLost || 0;
                    videoPacketsReceived += report.packetsReceived || 0;
                    jitter = report.jitter || 0;
                    frameRate = report.framesPerSecond || 0;
                    if (report.frameWidth && report.frameHeight) {
                        resolution = `${report.frameWidth}x${report.frameHeight}`;
                    }
                }
                else if (report.kind === 'audio') {
                    audioPacketsLost += report.packetsLost || 0;
                    audioPacketsReceived += report.packetsReceived || 0;
                }
            }
            else if (report.type === 'outbound-rtp') {
                bandwidth = report.bytesSent || 0;
            }
            else if (report.type === 'candidate-pair' && report.state === 'succeeded') {
                currentRTT = report.currentRoundTripTime ? report.currentRoundTripTime * 1000 : 0;
                connectionType = report.transportId || 'unknown';
            }
            else if (report.type === 'local-candidate' && report.candidateType) {
                candidateType = report.candidateType;
            }
            else if (report.type === 'codec') {
                codecName = report.mimeType || '';
            }
        });
        // 计算丢包率
        const videoLossRate = videoPacketsReceived > 0 ? (videoPacketsLost / (videoPacketsLost + videoPacketsReceived)) * 100 : 0;
        const audioLossRate = audioPacketsReceived > 0 ? (audioPacketsLost / (audioPacketsLost + audioPacketsReceived)) * 100 : 0;
        // 输出统计信息
        this.logger.info('📊 WebRTC统计信息', {
            remoteUserId,
            rtt: `${currentRTT.toFixed(1)}ms`,
            videoLossRate: `${videoLossRate.toFixed(2)}%`,
            audioLossRate: `${audioLossRate.toFixed(2)}%`,
            connectionType,
            candidateType,
            jitter: `${(jitter * 1000).toFixed(1)}ms`,
            frameRate: `${frameRate}fps`,
            resolution,
            codecName,
            bandwidth: `${(bandwidth / 1000).toFixed(1)}KB/s`
        });
        // 全面的高延迟检测和智能提示
        this._detectLatencyIssues({
            rtt: currentRTT,
            videoLossRate,
            audioLossRate,
            jitter,
            candidateType,
            codecName,
            frameRate,
            resolution,
            bandwidth
        });
    }
    /**
     * 全面检测WebRTC延迟问题并提供智能提示
     * @param stats 统计数据
     */
    _detectLatencyIssues(stats) {
        // 打印所有检测数据
        this.logger.info('🔍 延迟检测输入数据', {
            rtt: `${stats.rtt.toFixed(1)}ms`,
            videoLossRate: `${stats.videoLossRate.toFixed(2)}%`,
            audioLossRate: `${stats.audioLossRate.toFixed(2)}%`,
            jitter: `${(stats.jitter * 1000).toFixed(1)}ms`,
            candidateType: stats.candidateType,
            codecName: stats.codecName,
            frameRate: `${stats.frameRate}fps`,
            resolution: stats.resolution,
            bandwidth: `${(stats.bandwidth / 1000).toFixed(1)}KB/s`
        });
        const issues = [];
        // 1. 网络延迟检测
        if (stats.rtt > 200) {
            issues.push({
                type: 'high-network-latency',
                severity: 'high',
                message: `网络延迟过高: ${stats.rtt.toFixed(0)}ms`,
                impact: '严重影响实时通信体验',
                causes: ['网络拥塞', '路由距离过远', '网络质量差'],
                solutions: [
                    '检查网络连接质量',
                    '尝试切换到更稳定的网络',
                    '联系网络服务提供商'
                ]
            });
        }
        else if (stats.rtt > 100) {
            issues.push({
                type: 'medium-network-latency',
                severity: 'medium',
                message: `网络延迟较高: ${stats.rtt.toFixed(0)}ms`,
                impact: '可能影响通信流畅度',
                solutions: ['优化网络环境', '关闭其他占用带宽的应用']
            });
        }
        // 2. 丢包率检测
        if (stats.videoLossRate > 10 || stats.audioLossRate > 5) {
            issues.push({
                type: 'high-packet-loss',
                severity: 'high',
                message: `丢包率过高: 视频${stats.videoLossRate.toFixed(1)}%, 音频${stats.audioLossRate.toFixed(1)}%`,
                impact: '导致音视频卡顿和质量下降',
                causes: ['网络不稳定', '带宽不足', '网络拥塞'],
                solutions: [
                    '检查网络稳定性',
                    '降低视频质量设置',
                    '使用有线网络连接'
                ]
            });
        }
        // 3. 网络抖动检测
        if (stats.jitter > 0.05) {
            issues.push({
                type: 'high-jitter',
                severity: 'medium',
                message: `网络抖动过高: ${(stats.jitter * 1000).toFixed(1)}ms`,
                impact: '导致音视频不同步和卡顿',
                causes: ['网络不稳定', 'WiFi信号弱', '网络设备性能不足'],
                solutions: [
                    '使用有线网络',
                    '靠近WiFi路由器',
                    '重启网络设备'
                ]
            });
        }
        // 4. 编解码器效率检测
        if (stats.codecName.includes('VP8') || stats.codecName.includes('VP9')) {
            issues.push({
                type: 'inefficient-codec',
                severity: 'medium',
                message: '使用了高延迟编解码器',
                impact: '编解码延迟增加50-100ms',
                causes: ['浏览器默认选择', '设备不支持硬件加速'],
                solutions: [
                    '优先使用H.264编解码器',
                    '启用硬件加速',
                    '更新浏览器版本'
                ]
            });
        }
        // 5. 帧率过低检测
        if (stats.frameRate > 0 && stats.frameRate < 15) {
            issues.push({
                type: 'low-framerate',
                severity: 'medium',
                message: `帧率过低: ${stats.frameRate}fps`,
                impact: '视频不流畅,感知延迟增加',
                causes: ['设备性能不足', 'CPU占用过高', '编码器负载过重'],
                solutions: [
                    '降低视频分辨率',
                    '关闭其他占用CPU的程序',
                    '使用硬件编码'
                ]
            });
        }
        // 6. 分辨率过高检测
        // if (stats.resolution && this._isHighResolution(stats.resolution)) {
        //   issues.push({
        //     type: 'high-resolution',
        //     severity: 'low',
        //     message: `分辨率较高: ${stats.resolution}`,
        //     impact: '增加编码延迟和带宽需求',
        //     solutions: [
        //       '降低视频分辨率到720p或480p',
        //       '根据网络条件自适应调整'
        //     ]
        //   });
        // }
        // 7. 带宽检测(暂时禁用,因为当前实现有误)
        // TODO: 需要实现真正的带宽速率计算,而不是使用累计字节数
        // 当前的 stats.bandwidth 是累计发送字节数,不是实时速率
        // 正确的实现需要:(当前字节数 - 上次字节数) / 时间间隔
        /*
        if (stats.bandwidth > 0 && stats.bandwidth < 500000) {
          issues.push({
            type: 'insufficient-bandwidth',
            severity: 'high',
            message: '可用带宽不足',
            impact: '导致视频质量下降和延迟增加',
            causes: ['网络带宽限制', '多设备共享网络', '网络拥塞'],
            solutions: [
              '关闭其他占用带宽的应用',
              '使用QoS优化网络',
              '升级网络套餐'
            ]
          });
        }
        */
        // 8. ICE连接类型检测
        if (stats.candidateType === 'relay') {
            issues.push({
                type: 'turn-relay-connection',
                severity: 'high',
                message: '使用TURN中继连接',
                impact: '所有数据通过服务器中转,延迟200-500ms',
                causes: ['严格的NAT/防火墙', '网络配置限制'],
                solutions: [
                    '配置端口转发',
                    '调整防火墙设置',
                    '使用更近的TURN服务器'
                ]
            });
        }
        // 发送所有检测到的问题
        if (issues.length > 0) {
            this.emit('latency-issues-detected', {
                timestamp: Date.now(),
                totalIssues: issues.length,
                issues: issues
            });
            // 记录警告日志
            this.logger.warn('🔍 检测到WebRTC延迟问题', {
                count: issues.length,
                types: issues.map(i => i.type),
                details: issues.map(i => ({
                    type: i.type,
                    severity: i.severity,
                    message: i.message
                }))
            });
        }
    }
    /**
     * 检查是否为高分辨率
     * @param resolution 分辨率字符串
     */
    _isHighResolution(resolution) {
        const match = resolution.match(/(\d+)x(\d+)/);
        if (match) {
            const width = parseInt(match[1]);
            const height = parseInt(match[2]);
            return width > 1280 || height > 720; // 大于720p认为是高分辨率
        }
        return false;
    }
    /**
     * 分析ICE候选并检测延迟问题
     * @param candidate ICE候选
     */
    _analyzeICECandidate(candidate) {
        const candidateStr = candidate.candidate;
        const issues = [];
        // 1. mDNS地址检测(最关键的延迟问题)
        if (candidateStr.includes('.local')) {
            issues.push({
                type: 'mdns-detected',
                severity: 'high',
                message: '检测到mDNS地址,无法直连',
                impact: '必须通过STUN中转,延迟增加150-300ms',
                causes: ['浏览器IP匿名化功能', 'Chrome隐私设置'],
                solutions: [
                    {
                        title: '关闭Chrome IP匿名化',
                        description: '在chrome://flags/中禁用WebRTC IP处理策略',
                        steps: [
                            '打开chrome://flags/#enable-webrtc-hide-local-ips-with-mdns',
                            '设置为Disabled',
                            '重启浏览器'
                        ]
                    },
                    {
                        title: '使用Firefox',
                        description: 'Firefox默认不使用mDNS,延迟更低'
                    }
                ]
            });
        }
        // 2. TURN中继连接检测
        if (candidate.type === 'relay') {
            issues.push({
                type: 'turn-relay-detected',
                severity: 'high',
                message: '使用TURN中继连接',
                impact: '所有数据通过服务器中转,延迟200-500ms',
                causes: ['严格的NAT设置', '防火墙限制', '企业网络环境'],
                solutions: [
                    '检查防火墙设置,允许UDP流量',
                    '配置端口转发或UPnP',
                    '使用更近的TURN服务器',
                    '联系网络管理员优化网络配置'
                ]
            });
        }
        // 3. STUN反射连接检测
        if (candidate.type === 'srflx') {
            const address = candidate.address;
            if (address && this._isPrivateIP(address)) {
                // 在同一局域网但使用STUN反射
                issues.push({
                    type: 'unnecessary-stun',
                    severity: 'medium',
                    message: '同一局域网使用STUN反射',
                    impact: '不必要的延迟增加50-150ms',
                    causes: ['NAT设置问题', '网络配置不当'],
                    solutions: [
                        '检查路由器NAT设置',
                        '优化ICE配置优先使用本地候选',
                        '使用有线连接替代WiFi'
                    ]
                });
            }
        }
        // 4. 端口范围检测
        const portMatch = candidateStr.match(/:(\d+)/);
        if (portMatch) {
            const port = parseInt(portMatch[1]);
            if (port < 1024) {
                issues.push({
                    type: 'privileged-port',
                    severity: 'low',
                    message: `使用特权端口: ${port}`,
                    impact: '可能影响连接建立速度',
                    solutions: ['检查系统端口配置']
                });
            }
        }
        // 5. IPv6连接检测
        if (candidateStr.includes(':') && candidateStr.split(':').length > 3) {
            // 简单的IPv6检测
            issues.push({
                type: 'ipv6-connection',
                severity: 'low',
                message: '使用IPv6连接',
                impact: '在某些网络环境下可能影响连接性能',
                solutions: ['检查IPv6网络配置和支持']
            });
        }
        // 发送ICE候选相关的问题
        if (issues.length > 0) {
            this.emit('ice-candidate-issues', {
                timestamp: Date.now(),
                candidate: {
                    type: candidate.type,
                    address: candidate.address,
                    port: candidate.port,
                    protocol: candidate.protocol
                },
                issues: issues
            });
            // 记录警告日志
            this.logger.warn('⚠️ ICE候选延迟问题', {
                candidateType: candidate.type,
                issueCount: issues.length,
                types: issues.map(i => i.type)
            });
        }
        else {
            // 记录正常的候选
            this.logger.debug('✅ 检测到优质ICE候选', {
                type: candidate.type,
                address: candidate.address
            });
        }
    }
    /**
     * 检查是否为私有IP地址
     * @param ip IP地址
     */
    _isPrivateIP(ip) {
        // IPv4私有地址范围
        const privateRanges = [
            /^10\./,
            /^172\.(1[6-9]|2[0-9]|3[01])\./,
            /^192\.168\./,
            /^127\./,
            /^169\.254\./, // 169.254.0.0/16 (link-local)
        ];
        return privateRanges.some(range => range.test(ip));
    }
    /**
     * 使用RTCRtpSender.setCodecPreferences强制H.264编解码器
     * @param peerConnection RTCPeerConnection实例
     */
    _setH264CodecPreferences(peerConnection) {
        try {
            // 获取所有发送器
            const senders = peerConnection.getSenders();
            senders.forEach(sender => {
                var _a, _b, _c;
                if (sender.track && sender.track.kind === 'video') {
                    // 获取支持的编解码器
                    const capabilities = RTCRtpSender.getCapabilities('video');
                    if (!capabilities || !capabilities.codecs) {
                        this.logger.warn('⚠️ 浏览器不支持获取编解码器能力');
                        return;
                    }
                    // 查找H.264编解码器
                    const h264Codecs = capabilities.codecs.filter(codec => codec.mimeType.toLowerCase().includes('h264'));
                    if (h264Codecs.length === 0) {
                        this.logger.warn('⚠️ 浏览器不支持H.264编解码器');
                        return;
                    }
                    // 按优先级排序H.264编解码器(优先选择硬件加速的profile)
                    const sortedH264Codecs = h264Codecs.sort((a, b) => {
                        var _a, _b, _c, _d;
                        // 优先选择带有硬件加速标识的编解码器
                        const aHardware = ((_a = a.sdpFmtpLine) === null || _a === void 0 ? void 0 : _a.includes('profile-level-id=42e01f')) ||
                            ((_b = a.sdpFmtpLine) === null || _b === void 0 ? void 0 : _b.includes('profile-level-id=42001f'));
                        const bHardware = ((_c = b.sdpFmtpLine) === null || _c === void 0 ? void 0 : _c.includes('profile-level-id=42e01f')) ||
                            ((_d = b.sdpFmtpLine) === null || _d === void 0 ? void 0 : _d.includes('profile-level-id=42001f'));
                        if (aHardware && !bHardware)
                            return -1;
                        if (!aHardware && bHardware)
                            return 1;
                        return 0;
                    });
                    // 获取其他编解码器作为备选
                    const otherCodecs = capabilities.codecs.filter(codec => !codec.mimeType.toLowerCase().includes('h264'));
                    // 构建编解码器偏好列表:H.264优先,其他作为备选
                    const codecPreferences = [...sortedH264Codecs, ...otherCodecs];
                    // 设置编解码器偏好
                    if ('setCodecPreferences' in sender) {
                        // 浏览器支持setCodecPreferences方法
                        sender.setCodecPreferences(codecPreferences);
                        this.logger.info('✅ 已设置H.264编解码器偏好', {
                            h264Count: h264Codecs.length,
                            totalCodecs: codecPreferences.length,
                            preferredCodec: (_a = sortedH264Codecs[0]) === null || _a === void 0 ? void 0 : _a.mimeType,
                            hardwareAccelerated: (_c = (_b = sortedH264Codecs[0]) === null || _b === void 0 ? void 0 : _b.sdpFmtpLine) === null || _c === void 0 ? void 0 : _c.includes('profile-level-id=42e01f')
                        });
                    }
                    else {
                        this.logger.warn('⚠️ 浏览器不支持setCodecPreferences方法,将使用SDP修改方式');
                        // 备用方案:标记需要使用SDP修改
                        peerConnection._needsH264SdpModification = true;
                    }
                }
            });
        }
        catch (error) {
            this.logger.error('❌ 设置H.264编解码器偏好失败', error);
        }
    }
    /**
     * 在添加轨道后设置编解码器偏好
     * @param peerConnection RTCPeerConnection实例
     * @param stream 媒体流
     */
    _addStreamWithH264Preference(peerConnection, stream) {
        // 添加轨道到peer connection
        stream.getTracks().forEach(track => {
            peerConnection.addTrack(track, stream);
        });
        // 设置H.264编解码器偏好
        this._setH264CodecPreferences(peerConnection);
    }
    /**
     * 强制使用H.264编解码器(SDP修改方式)
     * @param sessionDescription SDP描述
     * @returns 修改后的SDP描述
     */
    _forceH264CodecViaSDP(sessionDescription) {
        if (!sessionDescription.sdp) {
            return sessionDescription;
        }
        let sdp = sessionDescription.sdp;
        // 查找H.264编解码器的payload type
        const h264PayloadTypes = [];
        const lines = sdp.split('\n');
        // 找到所有H.264相关的payload type
        lines.forEach(line => {
            if (line.includes('a=rtpmap:') && line.toLowerCase().includes('h264')) {
                const match = line.match(/a=rtpmap:(\d+)/);
                if (match) {
                    h264PayloadTypes.push(match[1]);
                }
            }
        });
        if (h264PayloadTypes.length === 0) {
            this.logger.warn('⚠️ 未找到H.264编解码器,无法强制使用');
            return sessionDescription;
        }
        // 修改m=video行,将H.264 payload type放在最前面
        const modifiedLines = lines.map(line => {
            if (line.startsWith('m=video')) {
                const parts = line.split(' ');
                if (parts.length > 3) {
                    // 提取所有payload types
                    const payloadTypes = parts.slice(3);
                    // 将H.264 payload types放在最前面
                    const h264Types = payloadTypes.filter(pt => h264PayloadTypes.includes(pt));
                    const otherTypes = payloadTypes.filter(pt => !h264PayloadTypes.includes(pt));
                    // 重新构建m=video行
                    const newPayloadTypes = [...h264Types, ...otherTypes];
                    return `${parts.slice(0, 3).join(' ')} ${newPayloadTypes.join(' ')}`;
                }
            }
            return line;
        });
        const modifiedSdp = modifiedLines.join('\n');
        this.logger.info('✅ 已通过SDP修改强制使用H.264编解码器', {
            h264PayloadTypes,
            originalCodecs: this._extractVideoCodecs(sdp),
            modifiedCodecs: this._extractVideoCodecs(modifiedSdp)
        });
        return Object.assign(Object.assign({}, sessionDescription), { sdp: modifiedSdp });
    }
    /**
     * 提取SDP中的视频编解码器信息
     * @param sdp SDP字符串
     * @returns 编解码器列表
     */
    _extractVideoCodecs(sdp) {
        const codecs = [];
        const lines = sdp.split('\n');
        lines.forEach(line => {
            if (line.includes('a=rtpmap:') && (line.includes('video') ||
                line.toLowerCase().includes('h264') ||
                line.toLowerCase().includes('vp8') ||
                line.toLowerCase().includes('vp9') ||
                line.toLowerCase().includes('av1'))) {
                const match = line.match(/a=rtpmap:\d+ ([^/]+)/);
                if (match) {
                    codecs.push(match[1]);
                }
            }
        });
        return codecs;
    }
}