yirtc-sdk-web
Version:
基于WebRTC的实时音视频SDK
1,431 lines • 60.7 kB
JavaScript
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;
}
}