2026-04-29 15:18:30 +08:00
/ * *
* 状态管理
* 使用简单的 Observable 模式 , 可替换为 Redux / Vuex / Pinia
* /
import { mockCallSession } from './models.js' ;
import { Signaling , WebSocketSignaling } from "../../module/signaling.js" ; // 信令管理
import { RenderStreaming } from "../../module/renderstreaming.js" ; // WebRTC连接管理
import { getServerConfig , getRTCConfiguration } from "../js/config.js" ; //服务器配置和RTC配置
import { showNotification , generateId } from './utils.js' ; // 导入通知函数
import chatMessage from './chatmessage.js' ;
2026-05-24 00:54:58 +08:00
import {
DEFAULT _PARTICIPANT _AVATAR ,
DEFAULT _PARTICIPANT _NAME ,
buildParticipantsSyncData ,
omitParticipant ,
removeParticipant ,
upsertParticipant
} from './participants.js' ;
2026-04-29 15:18:30 +08:00
const AUDIO _CONFIG = {
echoCancellation : true ,
noiseSuppression : true ,
autoGainControl : true
} ;
const VAD _CONFIG = {
threshold : 15 ,
debounceTime : 500 ,
fftSize : 256
} ;
const MEDIA _CONSTRAINTS = {
video : {
width : { ideal : 1920 , max : 1920 } ,
height : { ideal : 1080 , max : 1080 } ,
frameRate : { ideal : 30 , max : 30 }
} ,
audio : AUDIO _CONFIG
} ;
const VIDEO _ONLY _CONSTRAINT = {
video : {
width : { ideal : 1920 , max : 1920 } ,
height : { ideal : 1080 , max : 1080 } ,
frameRate : { ideal : 30 , max : 30 }
} ,
audio : false
} ;
class CallStateManager {
constructor ( ) {
// 核心状态
this . state = {
id : generateId ( ) ,
session : {
... mockCallSession ,
status : 'idle' // 初始状态为空闲
} ,
localStream : null , // MediaStream 对象
remoteStream : null , // 单路远端流( 兼容旧逻辑, participant端使用)
remoteStreams : { } , // 多路远端流 Map: { connectionId: MediaStream }( host端使用)
participants : { } // 多Participant用户信息 Map: { participantId: { id, name, avatar, mediaState, status } }( host端使用)
} ;
// 监听器数组
this . listeners = [ ] ;
2026-05-18 23:03:28 +08:00
this . socketEventHandlers = { } ;
this . _socketInviteBound = false ;
2026-04-29 15:18:30 +08:00
}
// 订阅状态变化
subscribe ( callback ) {
this . listeners . push ( callback ) ;
return ( ) => {
this . listeners = this . listeners . filter ( cb => cb !== callback ) ;
} ;
}
// 通知所有监听器
notify ( changes ) {
this . listeners . forEach ( cb => cb ( this . state , changes ) ) ;
}
// 初始化
async init ( ) {
// 初始化配置
await this . setupConfig ( ) ;
// 加载用户设置
this . loadUserSettings ( ) ;
// 获取本地摄像头视频流
await this . getLocalStream ( ) ;
}
// 加载用户设置
loadUserSettings ( ) {
const userSettings = localStorage . getItem ( 'userSettings' ) ;
if ( userSettings ) {
try {
const settings = JSON . parse ( userSettings ) ;
// 更新本地用户信息
if ( settings . name || settings . avatar ) {
this . state . session . localUser = {
... this . state . session . localUser ,
id : settings . userId || this . state . session . localUser . id ,
name : settings . name || this . state . session . localUser . name ,
avatar : settings . avatar || this . state . session . localUser . avatar
} ;
// 通知UI更新
this . notify ( { type : 'USER_SETTINGS_UPDATED' , user : this . state . session . localUser } ) ;
}
// 恢复保存的分辨率设置
if ( settings . resolution ) {
this . _savedResolution = settings . resolution ;
console . log ( ` 已恢复分辨率设置: ${ settings . resolution . width } x ${ settings . resolution . height } ` ) ;
}
} catch ( error ) {
console . error ( 'Error loading user settings:' , error ) ;
}
}
}
async setupConfig ( ) {
const res = await getServerConfig ( ) ;
this . useWebSocket = res . useWebSocket ;
}
// 获取本地摄像头视频流
async getLocalStream ( ) {
try {
console . log ( 'Requesting camera permission...' ) ;
// 检查浏览器是否支持getUserMedia
if ( ! navigator . mediaDevices || ! navigator . mediaDevices . getUserMedia ) {
console . error ( 'getUserMedia is not supported' ) ;
throw new Error ( 'getUserMedia is not supported' ) ;
}
// 请求摄像头权限并获取媒体流,启用回声消除
// 使用保存的分辨率(如有),否则使用默认约束
const videoConstraints = this . _savedResolution
? {
width : { ideal : this . _savedResolution . width , max : this . _savedResolution . width } ,
height : { ideal : this . _savedResolution . height , max : this . _savedResolution . height } ,
frameRate : { ideal : 30 , max : 30 }
}
: MEDIA _CONSTRAINTS . video ;
const stream = await navigator . mediaDevices . getUserMedia ( {
video : videoConstraints ,
audio : AUDIO _CONFIG
} ) ;
console . log ( 'Stream obtained successfully:' , stream ) ;
console . log ( 'Video tracks:' , stream . getVideoTracks ( ) ) ;
console . log ( 'Audio tracks:' , stream . getAudioTracks ( ) ) ;
this . state . localStream = stream ;
this . state . session . localUser . mediaState . video = true ;
this . state . session . localUser . mediaState . audio = true ;
console . log ( 'Local stream stored, notifying UI...' ) ;
// 先通知视频流已获取
this . notify ( { type : 'LOCAL_STREAM_OBTAINED' , stream } ) ;
// 再通知媒体状态变化
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'video' , value : true } ) ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'audio' , value : true } ) ;
// 发送媒体状态到服务器
this . emitMediaStateChange ( ) ;
// 启动本地音频活动检测
this . startActivityDetection ( this . state . localStream , { isLocal : true } ) ;
} catch ( error ) {
console . error ( 'Error getting local stream:' , error ) ;
// 如果获取视频失败,保持视频关闭状态
this . state . session . localUser . mediaState . video = false ;
this . state . session . localUser . mediaState . audio = false ;
// 通知媒体状态变化
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'video' , value : false } ) ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'audio' , value : false } ) ;
}
}
// 更新本地媒体状态
async updateLocalMedia ( mediaType , value ) {
// 如果是开启视频,重新获取摄像头资源
if ( mediaType === 'video' && value ) {
try {
// 只获取新的视频轨道,不干扰正在工作的音频
const newVideoStream = await navigator . mediaDevices . getUserMedia ( VIDEO _ONLY _CONSTRAINT ) ;
const newVideoTrack = newVideoStream . getVideoTracks ( ) [ 0 ] ;
if ( ! newVideoTrack ) {
throw new Error ( 'Failed to get video track' ) ;
}
// 更新本地流中的视频轨道(替换旧的已停止的轨道)
if ( this . state . localStream ) {
const oldVideoTracks = this . state . localStream . getVideoTracks ( ) ;
oldVideoTracks . forEach ( track => {
track . stop ( ) ;
this . state . localStream . removeTrack ( track ) ;
} ) ;
this . state . localStream . addTrack ( newVideoTrack ) ;
} else {
// 本地流不存在时(不应该发生),使用新流
this . state . localStream = newVideoStream ;
}
// 更新WebRTC连接中的视频轨道
if ( this . renderstreaming ) {
console . log ( 'Updating video track in WebRTC connection' ) ;
if ( this . role === 'host' ) {
// Host端: 需要遍历所有participant的peer来替换视频轨道
const participantIds = Object . keys ( this . state . remoteStreams ) ;
for ( const participantId of participantIds ) {
const transceivers = this . renderstreaming . getTransceivers ( participantId ) ;
if ( ! transceivers ) continue ;
const videoTransceivers = transceivers . filter ( t =>
t . sender && t . sender . track && t . sender . track . kind === 'video'
) ;
if ( videoTransceivers . length > 0 ) {
for ( const transceiver of videoTransceivers ) {
try {
await transceiver . sender . replaceTrack ( newVideoTrack ) ;
console . log ( ` Replaced video track for participant ${ participantId } ` ) ;
} catch ( error ) {
console . error ( ` Error replacing video track for ${ participantId } : ` , error ) ;
}
}
} else {
// 没有视频收发器,添加新的
try {
this . renderstreaming . addTransceiver ( newVideoTrack , { direction : 'sendonly' } , participantId ) ;
console . log ( ` Added new video transceiver for participant ${ participantId } ` ) ;
} catch ( error ) {
console . error ( ` Error adding video transceiver for ${ participantId } : ` , error ) ;
}
}
// 设置编解码器偏好
setTimeout ( ( ) => { this . setCodecPreferences ( participantId ) ; } , 100 ) ;
setTimeout ( ( ) => { this . setVideoEncodingParameters ( participantId ) ; } , 200 ) ;
}
} else {
// Participant端: 使用单一peer
const transceivers = this . renderstreaming . getTransceivers ( ) ;
if ( transceivers ) {
const videoTransceivers = transceivers . filter ( t =>
t . sender && t . sender . track && t . sender . track . kind === 'video'
) ;
if ( videoTransceivers . length > 0 ) {
for ( const transceiver of videoTransceivers ) {
try {
await transceiver . sender . replaceTrack ( newVideoTrack ) ;
console . log ( 'Successfully replaced video track' ) ;
} catch ( error ) {
console . error ( 'Error replacing video track:' , error ) ;
}
}
} else {
try {
this . renderstreaming . addTransceiver ( newVideoTrack , { direction : 'sendonly' } ) ;
console . log ( 'Added new video transceiver' ) ;
} catch ( error ) {
console . error ( 'Error adding video transceiver:' , error ) ;
}
}
}
setTimeout ( ( ) => { this . setCodecPreferences ( ) ; } , 100 ) ;
setTimeout ( ( ) => { this . setVideoEncodingParameters ( ) ; } , 200 ) ;
}
}
// 更新状态和通知UI
this . state . session . localUser . mediaState . video = true ;
this . notify ( { type : 'LOCAL_STREAM_OBTAINED' , stream : this . state . localStream } ) ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'video' , value : true } ) ;
this . emitMediaStateChange ( ) ;
this . startActivityDetection ( this . state . localStream , { isLocal : true } ) ;
} catch ( error ) {
console . error ( 'Error reopening video:' , error ) ;
this . state . session . localUser . mediaState . video = false ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'video' , value : false } ) ;
}
} else {
// 直接更新媒体状态
this . state . session . localUser . mediaState [ mediaType ] = value ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType , value } ) ;
// 发送媒体状态到服务器
this . emitMediaStateChange ( ) ;
}
// 如果是关闭视频,释放摄像头资源
if ( mediaType === 'video' && ! value && this . state . localStream ) {
this . state . session . localUser . mediaState . video = false ;
this . state . localStream . getTracks ( ) . forEach ( track => {
if ( track . kind === 'video' ) {
track . stop ( ) ;
}
} ) ;
// 发送媒体状态到服务器
this . emitMediaStateChange ( ) ;
}
// 如果是音频状态变化,控制本地音频轨道
if ( mediaType === 'audio' && this . state . localStream ) {
this . state . session . localUser . mediaState . audio = value ;
this . state . localStream . getTracks ( ) . forEach ( track => {
if ( track . kind === 'audio' ) {
track . enabled = value ;
}
} ) ;
// 发送媒体状态到服务器
this . emitMediaStateChange ( ) ;
}
// 通知UI更新用户列表
this . notify ( { type : 'USER_LIST_UPDATE' , localUser : this . state . session . localUser , remoteUser : this . state . session . remoteUser } ) ;
}
2026-05-16 21:26:19 +08:00
/ * *
* 早期连接WebSocket信令 ( 在connect视图调用 )
* 仅建立WebSocket连接 , 不创建 / 加入房间
* @ async
* @ returns { Promise < WebSocketSignaling | Signaling > } 信令实例
* /
async connectSignaling ( ) {
// 先获取配置
await this . setupConfig ( ) ;
if ( this . _signaling ) {
console . log ( 'Signaling already connected, reusing existing instance' ) ;
return this . _signaling ;
}
// 创建信令实例
this . _signaling = this . useWebSocket ? new WebSocketSignaling ( ) : new Signaling ( ) ;
await this . _signaling . start ( ) ;
2026-05-18 23:03:28 +08:00
this . _bindSocketInviteEvents ( this . _signaling ) ;
2026-05-16 21:26:19 +08:00
console . log ( 'Signaling connected (WebSocket only, no room yet)' ) ;
return this . _signaling ;
}
2026-05-18 23:03:28 +08:00
_bindSocketInviteEvents ( signaling ) {
if ( ! signaling || this . _socketInviteBound || typeof signaling . addEventListener !== 'function' ) {
return ;
}
[ 'invite-call' , 'invite-accepted' , 'invite-rejected' , 'invite-failed' ] . forEach ( ( eventName ) => {
signaling . addEventListener ( eventName , ( event ) => {
const handler = this . socketEventHandlers [ eventName ] ;
if ( typeof handler === 'function' ) {
handler ( event . detail ) ;
}
} ) ;
} ) ;
this . _socketInviteBound = true ;
}
onSocketEvent ( eventName , handler ) {
this . socketEventHandlers [ eventName ] = handler ;
}
getActiveSignaling ( ) {
if ( this . _signaling ) {
return this . _signaling ;
}
if ( this . renderstreaming && this . renderstreaming . _signaling ) {
return this . renderstreaming . _signaling ;
}
return null ;
}
sendInviteCall ( payload ) {
const signaling = this . getActiveSignaling ( ) ;
if ( ! signaling || typeof signaling . sendInviteCall !== 'function' ) {
throw new Error ( 'Invite signaling is not ready' ) ;
}
signaling . sendInviteCall ( payload ) ;
}
sendInviteAccepted ( payload ) {
const signaling = this . getActiveSignaling ( ) ;
if ( ! signaling || typeof signaling . sendInviteAccepted !== 'function' ) {
throw new Error ( 'Invite signaling is not ready' ) ;
}
signaling . sendInviteAccepted ( payload ) ;
}
sendInviteRejected ( payload ) {
const signaling = this . getActiveSignaling ( ) ;
if ( ! signaling || typeof signaling . sendInviteRejected !== 'function' ) {
throw new Error ( 'Invite signaling is not ready' ) ;
}
signaling . sendInviteRejected ( payload ) ;
}
2026-05-16 23:07:08 +08:00
/ * *
* 在仅建立WebSocket连接时同步当前用户信息
* @ param { { id ? : string , name ? : string , avatar ? : string } | null } userInfo - 用户信息
* /
syncSocketUserInfo ( userInfo = null ) {
const settings = userInfo || ( ( ) => {
try {
return JSON . parse ( localStorage . getItem ( 'userSettings' ) || '{}' ) ;
} catch ( error ) {
console . error ( 'Error parsing user settings:' , error ) ;
return { } ;
}
} ) ( ) ;
const payload = {
id : settings . id || settings . userId || this . state . session . localUser . id || '' ,
name : settings . name || this . state . session . localUser . name || '我' ,
avatar : settings . avatar || this . state . session . localUser . avatar || '/images/p1.png'
} ;
this . state . session . localUser = {
... this . state . session . localUser ,
id : payload . id ,
name : payload . name ,
avatar : payload . avatar
} ;
if ( this . _signaling && typeof this . _signaling . sendMessage === 'function' ) {
this . _signaling . sendMessage ( '' , {
type : 'user-info' ,
data : payload
} ) ;
}
}
2026-04-29 15:18:30 +08:00
/ * *
* 创建信令和RTC实例
* @ async
* @ param { string } connectionId - 连接ID
* @ returns { Promise < void > }
* /
async _createSignalingAndRTC ( connectionId ) {
this . connectionId = connectionId ; // 获取连接ID
// 设置状态为连接中
this . state . session . status = 'connecting' ;
this . notify ( { type : 'CALL_STATUS_CHANGE' , status : 'connecting' } ) ;
// 确保本地流已经初始化
if ( ! this . state . localStream ) {
console . log ( 'Local stream not available, waiting for initialization...' ) ;
// 等待localStream初始化
await new Promise ( ( resolve ) => {
const checkStream = ( ) => {
if ( this . state . localStream ) {
resolve ( ) ;
} else {
setTimeout ( checkStream , 100 ) ;
}
} ;
checkStream ( ) ;
} ) ;
}
2026-05-16 21:26:19 +08:00
// 复用已有信令实例( connectSignaling()已建立WebSocket) , 或创建新实例
const signaling = this . _signaling || ( this . useWebSocket ? new WebSocketSignaling ( ) : new Signaling ( ) ) ;
2026-04-29 15:18:30 +08:00
const config = getRTCConfiguration ( ) ; // 获取RTC配置
this . renderstreaming = new RenderStreaming ( signaling , config ) ;
2026-05-16 21:26:19 +08:00
this . _signaling = null ; // RenderStreaming 已接管信令,清除引用
2026-04-29 15:18:30 +08:00
}
/ * *
* 设置WebRTC连接
* @ async
* @ returns { Promise < void > }
* /
async setUp ( connectionId ) {
await this . _createSignalingAndRTC ( connectionId ) ;
this . _registerCallbacks ( ) ;
await this . _startConnection ( connectionId ) ;
}
/ * *
* 注册所有WebRTC回调
* /
_registerCallbacks ( ) {
this . renderstreaming . onNewPeer = ( participantId ) => {
console . log ( ` New peer created for ${ participantId } , adding local tracks ` ) ;
if ( this . state . localStream ) {
const tracks = this . state . localStream . getTracks ( ) ;
for ( const track of tracks ) {
this . renderstreaming . addTransceiver ( track , { direction : 'sendonly' } , participantId ) ;
}
this . setCodecPreferences ( participantId ) ;
this . setVideoEncodingParameters ( participantId ) ;
}
} ;
// 连接建立回调
this . renderstreaming . onConnect = ( connectionId , data ) => {
// 保存角色信息( host/participant)
if ( data && data . role ) {
this . role = data . role ;
// 更新localUser的isHost标志
this . state . session . localUser . isHost = ( this . role === 'host' ) ;
// 保存自身的participantId, 用于从participants-sync中过滤自身
if ( data . participantId ) {
this . selfParticipantId = data . participantId ;
}
console . log ( ` Connected as ${ this . role } , participantId: ${ this . selfParticipantId } ` ) ;
}
// 连接建立后, 更新状态为ongoing
this . state . session . status = 'ongoing' ;
this . notify ( { type : 'CALL_STATUS_CHANGE' , status : 'ongoing' } ) ;
// 参与者加入时默认静音:禁用音频轨道并更新状态
if ( this . role === 'participant' ) {
if ( this . state . localStream ) {
this . state . localStream . getAudioTracks ( ) . forEach ( track => {
track . enabled = false ;
} ) ;
}
this . state . session . localUser . mediaState . audio = false ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'audio' , value : false } ) ;
console . log ( 'Participant joined with audio muted by default' ) ;
}
// 连接建立后发送本地用户信息
this . sendMessage ( 'user-info' , {
id : this . state . session . localUser . id ,
name : this . state . session . localUser . name ,
avatar : this . state . session . localUser . avatar
} ) ;
// 发送当前媒体状态,确保远端收到正确的初始状态
this . emitMediaStateChange ( ) ;
if ( this . state . localStream ) {
// const tracks = this.state.localStream.getTracks();
// for (const track of tracks) {
// this.renderstreaming.addTransceiver(track, { direction: 'sendonly' });
// }
// this.setCodecPreferences();
this . showStatsMessage ( ) ;
} else {
console . error ( 'Local stream is not available' ) ;
showNotification ( '本地视频流不可用' , 'error' ) ;
}
} ;
// 连接断开回调(收到服务器的 disconnect 消息,通常是 host 离开导致房间关闭)
this . renderstreaming . onDisconnect = ( ) => {
console . log ( 'Received disconnect from server, host left or room closed' ) ;
this . hangUp ( ) ; // 房间已关闭,挂断连接
} ;
// SDP Answer 接收回调:重新设置编码参数以保障画质
this . renderstreaming . onGotAnswer = ( connectionId ) => {
console . log ( 'SDP Answer received, resetting encoding parameters for connectionId:' , connectionId ) ;
if ( this . role === 'host' ) {
const allParticipantIds = Object . keys ( this . state . remoteStreams || { } ) ;
for ( const pid of allParticipantIds ) {
setTimeout ( ( ) => { this . setVideoEncodingParameters ( pid ) ; } , 50 ) ;
}
} else {
setTimeout ( ( ) => { this . setVideoEncodingParameters ( ) ; } , 50 ) ;
}
} ;
// participant加入回调( host收到, 新participant加入房间)
this . renderstreaming . onParticipantJoined = ( participantId ) => {
console . log ( ` Participant joined: ${ participantId } ` ) ;
2026-05-24 00:54:58 +08:00
this . _upsertParticipant ( participantId ) ;
this . _notifyParticipantsUpdate ( ) ;
2026-04-29 15:18:30 +08:00
this . broadcastParticipantsList ( ) ;
} ;
// participant离开回调( host收到, 房间仍然存在)
this . renderstreaming . onParticipantLeft = ( participantId ) => {
console . log ( ` Participant left: ${ participantId } , room still active ` ) ;
this . updateRemoteUserStatus ( 'offline' ) ;
this . updateRemoteUserNetworkQuality ( 'no_signal' ) ;
showNotification ( '对方已离开通话' , 'warning' ) ;
// 清理该 participant 的远端流
if ( this . state . remoteStreams [ participantId ] ) {
this . state . remoteStreams [ participantId ] . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
delete this . state . remoteStreams [ participantId ] ;
}
if ( this . state . remoteStream ) {
this . state . remoteStream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
this . state . remoteStream = null ;
}
// 清理该 participant 的用户信息
2026-05-24 00:54:58 +08:00
this . _removeParticipant ( participantId ) ;
2026-04-29 15:18:30 +08:00
// 通知UI更新, 用participantId作为connectionId传给renderer
this . notify ( { type : 'PARTICIPANT_LEFT' , connectionId : participantId } ) ;
2026-05-24 00:54:58 +08:00
this . _notifyParticipantsUpdate ( ) ;
2026-04-29 15:18:30 +08:00
this . broadcastParticipantsList ( ) ;
} ;
// 轨道事件回调
this . renderstreaming . onTrackEvent = ( data ) => {
2026-05-24 00:54:58 +08:00
this . _handleTrackEvent ( data ) ;
2026-04-29 15:18:30 +08:00
} ;
this . renderstreaming . onMessage = ( data ) => {
2026-05-24 00:54:58 +08:00
this . _handleRenderStreamingMessage ( data ) ;
2026-04-29 15:18:30 +08:00
} ;
}
/ * *
* 启动WebRTC连接和检测
* @ async
* @ param { string } connectionId - 连接ID
* @ returns { Promise < void > }
* /
async _startConnection ( connectionId ) {
// 启动WebRTC连接
await this . renderstreaming . start ( ) ;
await this . renderstreaming . createConnection ( connectionId ) ;
// 启动网络质量检测
this . startNetworkQualityDetection ( ) ;
// 启动本地音频活动检测
this . startActivityDetection ( this . state . localStream , { isLocal : true } ) ;
//启动远端音频活动检测
this . startActivityDetection ( this . state . remoteStream , { isLocal : false } ) ;
}
/ * *
* 挂断WebRTC连接
* Host挂断 : 房间删除 , 通知所有participants
* Participant挂断 : 仅自己离开 , 房间保留
* @ async
* @ returns { Promise < void > }
* /
async hangUp ( ) {
this . clearStatsMessage ( ) ; // 清除统计信息
this . stopNetworkQualityDetection ( ) ; // 停止网络质量检测
// 停止通话时长计时器
if ( this . durationInterval ) {
clearInterval ( this . durationInterval ) ;
this . durationInterval = null ;
}
// 重置通话时长同步标志
this . durationSynced = false ;
const isHost = this . role === 'host' ;
console . log ( ` Disconnect peer on ${ this . connectionId } . Role: ${ this . role } ` ) ;
// 删除连接并停止WebRTC
if ( this . renderstreaming ) {
try {
// 发送断开连接信令给服务器
// 服务器会根据角色决定:
// - host断开: 通知所有participants, 删除房间
// - participant断开: 仅通知host, 保留房间
await this . renderstreaming . deleteConnection ( ) ;
await this . renderstreaming . stop ( ) ;
} catch ( error ) {
console . error ( 'Error during hangUp:' , error ) ;
}
this . renderstreaming = null ;
}
// 更新远程用户状态为离线
this . updateRemoteUserStatus ( 'offline' ) ;
this . updateRemoteUserNetworkQuality ( 'no_signal' ) ;
// 清理participants
this . state . participants = { } ;
this . selfParticipantId = null ;
this . connectionId = null ;
this . role = null ;
this . state . session . status = 'ended' ;
this . notify ( { type : 'CALL_ENDED' , reason : isHost ? 'host_hangup' : 'participant_hangup' } ) ;
}
2026-05-24 00:54:58 +08:00
_handleTrackEvent ( data ) {
const direction = data . transceiver . direction ;
if ( direction === 'sendrecv' || direction === 'recvonly' ) {
this . _handleIncomingTrack ( data ) ;
return ;
}
if ( direction === 'sendonly' && data . track . kind === 'audio' ) {
this . startActivityDetection ( this . state . localStream , { isLocal : true } ) ;
}
}
_handleIncomingTrack ( data ) {
const trackParticipantId = data . participantId || this . connectionId ;
const isHost = this . role === 'host' ;
const targetStream = this . _getOrCreateRemoteStream ( trackParticipantId , isHost ) ;
this . _replaceTrackOfSameKind ( targetStream , data . track ) ;
console . log ( 'Added new track:' , data . track . kind , 'for participant:' , trackParticipantId ) ;
if ( isHost && ! this . state . participants [ trackParticipantId ] ) {
this . _upsertParticipant ( trackParticipantId ) ;
this . _notifyParticipantsUpdate ( ) ;
this . broadcastParticipantsList ( ) ;
}
this . _notifyRemoteStreamUpdate ( targetStream , trackParticipantId , isHost , data . track . kind ) ;
if ( this . state . session . remoteUser . status !== 'online' ) {
this . updateRemoteUserStatus ( 'online' ) ;
this . updateRemoteUserNetworkQuality ( 'good' ) ;
this . sendMessage ( 'user-info' , {
id : this . state . session . localUser . id ,
name : this . state . session . localUser . name ,
avatar : this . state . session . localUser . avatar
} ) ;
this . _startDurationTimer ( ) ;
}
if ( data . track . kind === 'audio' ) {
this . startActivityDetection ( this . state . remoteStream , { isLocal : false } ) ;
}
}
_getOrCreateRemoteStream ( trackParticipantId , isHost ) {
if ( isHost ) {
if ( ! this . state . remoteStreams [ trackParticipantId ] ) {
this . state . remoteStreams [ trackParticipantId ] = new MediaStream ( ) ;
}
return this . state . remoteStreams [ trackParticipantId ] ;
}
if ( this . state . remoteStream == null ) {
this . state . remoteStream = new MediaStream ( ) ;
}
return this . state . remoteStream ;
}
_replaceTrackOfSameKind ( targetStream , track ) {
const existingTracks = targetStream . getTracks ( ) . filter ( existingTrack => existingTrack . kind === track . kind ) ;
existingTracks . forEach ( existingTrack => {
targetStream . removeTrack ( existingTrack ) ;
console . log ( 'Removed old track:' , existingTrack . kind ) ;
} ) ;
targetStream . addTrack ( track ) ;
}
_notifyRemoteStreamUpdate ( targetStream , trackParticipantId , isHost , trackKind ) {
const notifyStreamUpdate = ( ) => {
this . notify ( {
type : 'REMOTE_STREAM_OBTAINED' ,
stream : targetStream ,
connectionId : trackParticipantId ,
isHost
} ) ;
console . log ( 'Notified UI about remote stream update' ) ;
} ;
if ( trackKind === 'audio' && targetStream . getVideoTracks ( ) . length === 0 ) {
console . log ( 'Audio track arrived first, delaying stream notification for video track...' ) ;
setTimeout ( ( ) => {
const nowHasVideo = targetStream . getVideoTracks ( ) . length > 0 ;
console . log ( ` After delay, stream has video: ${ nowHasVideo } ` ) ;
notifyStreamUpdate ( ) ;
} , 200 ) ;
return ;
}
notifyStreamUpdate ( ) ;
}
_handleRenderStreamingMessage ( data ) {
console . log ( '收到消息:' , data ) ;
switch ( data . type ) {
case 'chat-message' :
this . _handleChatMessage ( data ) ;
break ;
case 'media-state-changed' :
this . _handleMediaStateChangedMessage ( data ) ;
break ;
case 'user-info' :
this . _handleUserInfoMessage ( data ) ;
break ;
case 'participants-sync' :
this . _handleParticipantsSyncMessage ( data ) ;
break ;
default :
break ;
}
}
_handleChatMessage ( data ) {
const chatPayload = data . data || data . message ;
if ( ! chatPayload ) {
return ;
}
chatMessage . handleChatMessage ( chatPayload ) ;
if ( data . participantId && this . role === 'host' && this . state . participants [ data . participantId ] ) {
this . _upsertParticipant ( data . participantId , {
id : chatPayload . senderId ,
... ( chatPayload . senderName ? { name : chatPayload . senderName } : { } ) ,
... ( chatPayload . senderAvatar ? { avatar : chatPayload . senderAvatar } : { } )
} ) ;
this . _notifyParticipantsUpdate ( ) ;
this . broadcastParticipantsList ( ) ;
return ;
}
if ( ! this . role || this . role !== 'host' ) {
if ( data . participantId && this . state . participants [ data . participantId ] ) {
this . _upsertParticipant ( data . participantId , {
... ( chatPayload . senderName ? { name : chatPayload . senderName } : { } ) ,
... ( chatPayload . senderAvatar ? { avatar : chatPayload . senderAvatar } : { } )
} ) ;
this . _notifyParticipantsUpdate ( ) ;
} else if ( chatPayload . senderId !== this . state . session . localUser . id ) {
this . _updateRemoteUserProfile ( {
id : chatPayload . senderId ,
name : chatPayload . senderName ,
avatar : chatPayload . senderAvatar
} ) ;
}
}
}
_handleMediaStateChangedMessage ( data ) {
console . log ( '收到媒体状态变化:' , data . data , 'from participant:' , data . participantId ) ;
if ( this . role === 'host' ) {
if ( data . participantId && this . state . participants [ data . participantId ] ) {
this . _upsertParticipant ( data . participantId , {
mediaState : data . data
} ) ;
}
this . updateRemoteMedia ( data . data , data . participantId ) ;
this . _notifyParticipantsUpdate ( ) ;
this . broadcastParticipantsList ( ) ;
return ;
}
if ( data . participantId && data . participantId !== this . selfParticipantId && this . state . participants [ data . participantId ] ) {
this . _upsertParticipant ( data . participantId , {
mediaState : data . data
} ) ;
this . _notifyParticipantsUpdate ( ) ;
return ;
}
if ( data . participantId === this . selfParticipantId ) {
return ;
}
console . log ( 'Received media-state-changed from Host, updating remoteUser:' , data . data ) ;
this . updateRemoteMedia ( data . data , data . participantId ) ;
this . _notifyParticipantsUpdate ( ) ;
}
_handleUserInfoMessage ( data ) {
console . log ( '收到用户信息:' , data . data , 'from participant:' , data . participantId ) ;
if ( ! data . data ) {
return ;
}
if ( data . participantId && this . role === 'host' ) {
this . _upsertParticipant ( data . participantId , {
id : data . data . id || '' ,
name : data . data . name || DEFAULT _PARTICIPANT _NAME ,
avatar : data . data . avatar || DEFAULT _PARTICIPANT _AVATAR
} ) ;
this . _notifyParticipantsUpdate ( ) ;
this . broadcastParticipantsList ( ) ;
return ;
}
this . _updateRemoteUserProfile ( {
id : data . data . id || this . state . session . remoteUser . id ,
name : data . data . name || this . state . session . remoteUser . name ,
avatar : data . data . avatar || this . state . session . remoteUser . avatar
} ) ;
}
_handleParticipantsSyncMessage ( data ) {
if ( this . role === 'host' || ! data . data ) {
return ;
}
console . log ( '收到成员列表同步:' , data . data ) ;
this . state . participants = omitParticipant ( data . data , this . selfParticipantId ) ;
this . _notifyParticipantsUpdate ( ) ;
this . _syncCallDuration ( data . callDuration ) ;
}
_updateRemoteUserProfile ( profile ) {
this . state . session . remoteUser = {
... this . state . session . remoteUser ,
... profile
} ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , mediaState : this . state . session . remoteUser . mediaState } ) ;
}
_syncCallDuration ( callDuration ) {
if ( this . durationSynced || typeof callDuration !== 'number' ) {
return ;
}
this . state . session . duration = callDuration ;
this . durationSynced = true ;
this . _startDurationTimer ( ) ;
this . notify ( { type : 'DURATION_UPDATE' , duration : this . state . session . duration } ) ;
console . log ( ` 通话时长已同步,当前时长: ${ callDuration } 秒 ` ) ;
}
_startDurationTimer ( ) {
if ( this . durationInterval ) {
return ;
}
this . durationInterval = setInterval ( ( ) => {
this . state . session . duration ++ ;
this . notify ( { type : 'DURATION_UPDATE' , duration : this . state . session . duration } ) ;
} , 1000 ) ;
}
2026-04-29 15:18:30 +08:00
/ * *
* 发送消息
* @ param { string } type - 消息类型
* @ param { Object } data - 消息数据
* /
sendMessage ( type , data ) {
if ( this . renderstreaming ) {
this . renderstreaming . sendMessage ( {
type : type ,
data : data
} ) ;
}
}
2026-05-24 00:54:58 +08:00
/ * *
* Participant state helpers
* /
_notifyParticipantsUpdate ( ) {
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
}
_upsertParticipant ( participantId , patch = { } ) {
return upsertParticipant ( this . state . participants , participantId , patch ) ;
}
_removeParticipant ( participantId ) {
return removeParticipant ( this . state . participants , participantId ) ;
}
2026-04-29 15:18:30 +08:00
/ * *
* Host端广播完整成员列表给所有Participant
* 包含Host自身信息 + 所有Participant信息
* Participant收到后可展示完整通话成员列表
* /
broadcastParticipantsList ( ) {
if ( this . role !== 'host' || ! this . renderstreaming ) return ;
2026-05-24 00:54:58 +08:00
const memberList = buildParticipantsSyncData ( this . state . session . localUser , this . state . participants ) ;
2026-04-29 15:18:30 +08:00
this . renderstreaming . sendMessage ( {
type : 'participants-sync' ,
data : memberList ,
callDuration : this . state . session . duration
} ) ;
console . log ( 'Broadcast participants list:' , Object . keys ( memberList ) ) ;
}
/ * *
* 设置编解码器偏好
* 优先选择 VP9 / AV1 ( 更高效的压缩 ) , 回退到 H264 High Profile
* /
setCodecPreferences ( participantId ) {
const capabilities = RTCRtpSender . getCapabilities ( 'video' ) ;
if ( ! capabilities || ! capabilities . codecs || capabilities . codecs . length === 0 ) return ;
const { codecs } = capabilities ;
// 构建多codec优先级列表( 而非只选一个)
let selectedCodecs = [ ] ;
const av1Codec = codecs . find ( c => c . mimeType === 'video/AV1' ) ;
const vp9Codec = codecs . find ( c => c . mimeType === 'video/VP9' ) ;
const h264HighCodec = codecs . find ( c =>
c . mimeType === 'video/H264' &&
c . sdpFmtpLine && c . sdpFmtpLine . includes ( 'profile-level-id=6400' )
) ;
const h264Codec = codecs . find ( c => c . mimeType === 'video/H264' ) ;
if ( av1Codec ) selectedCodecs . push ( av1Codec ) ;
if ( vp9Codec ) selectedCodecs . push ( vp9Codec ) ;
if ( h264HighCodec ) selectedCodecs . push ( h264HighCodec ) ;
if ( h264Codec && ( ! h264HighCodec || h264Codec !== h264HighCodec ) ) selectedCodecs . push ( h264Codec ) ;
if ( selectedCodecs . length === 0 ) return ;
if ( this . renderstreaming ) {
const transceivers = this . renderstreaming . getTransceivers ( participantId ) ;
if ( transceivers && transceivers . length > 0 ) {
const videoTransceivers = transceivers . filter ( t => {
if ( t . sender && t . sender . track ) {
return t . sender . track . kind === 'video' ;
}
return t . mid !== null && t . receiver && t . receiver . track && t . receiver . track . kind === 'video' ;
} ) ;
if ( videoTransceivers && videoTransceivers . length > 0 ) {
videoTransceivers . forEach ( t => {
try {
t . setCodecPreferences ( selectedCodecs ) ;
} catch ( e ) {
console . error ( 'Error setting codec preferences:' , e ) ;
}
} ) ;
console . log ( ` Codec preferences set: ${ selectedCodecs . map ( c => c . mimeType ) . join ( ' > ' ) } ` ) ;
}
}
}
}
/ * *
* 设置视频发送编码参数
* 提升 maxBitrate 以改善远端视频画质
* @ param { string } [ participantId ] - 目标participant ( host端使用 )
* /
setVideoEncodingParameters ( participantId ) {
if ( ! this . renderstreaming ) return ;
const transceivers = this . renderstreaming . getTransceivers ( participantId ) ;
if ( ! transceivers || transceivers . length === 0 ) return ;
const videoTransceivers = transceivers . filter ( t =>
t . sender && t . sender . track && t . sender . track . kind === 'video'
) ;
for ( const transceiver of videoTransceivers ) {
try {
const sender = transceiver . sender ;
const params = sender . getParameters ( ) ;
if ( ! params . encodings || params . encodings . length === 0 ) {
params . encodings = [ { } ] ;
}
// 根据实际采集分辨率动态设置maxBitrate
const videoTrack = sender . track ;
const settings = videoTrack ? videoTrack . getSettings ( ) : { } ;
const height = settings . height || 1080 ;
const bitrateMap = {
270 : 1000000 ,
480 : 1500000 ,
720 : 2500000 ,
1080 : 4000000 ,
1440 : 6000000
} ;
// 找到最接近的分辨率对应的比特率
let maxBitrate = 4000000 ;
const heights = Object . keys ( bitrateMap ) . map ( Number ) . sort ( ( a , b ) => a - b ) ;
for ( const h of heights ) {
if ( height <= h ) {
maxBitrate = bitrateMap [ h ] ;
break ;
}
maxBitrate = bitrateMap [ h ] ;
}
params . encodings [ 0 ] . maxBitrate = maxBitrate ;
params . encodings [ 0 ] . scaleResolutionDownBy = 1.0 ;
params . encodings [ 0 ] . xGoogleMinBitrate = Math . floor ( maxBitrate * 0.5 ) ;
// 优先保持分辨率,降低帧率来适应带宽
// 'maintain-resolution' 在带宽不足时保持清晰度
if ( params . degradationPreference !== undefined ) {
params . degradationPreference = 'maintain-resolution' ;
}
sender . setParameters ( params ) ;
console . log ( ` Set video encoding: maxBitrate= ${ maxBitrate / 1000000 } Mbps, scaleResolutionDownBy=1.0, xGoogleMinBitrate= ${ Math . floor ( maxBitrate * 0.5 ) } ${ participantId ? ` for ${ participantId } ` : '' } ` ) ;
} catch ( error ) {
console . error ( 'Error setting video encoding parameters:' , error ) ;
}
}
}
/ * *
* 动态切换视频分辨率
* 使用 MediaStreamTrack . applyConstraints ( ) 在通话中实时调整
* 同时根据分辨率调整编码比特率
* @ param { number } width - 目标宽度
* @ param { number } height - 目标高度
* /
async changeResolution ( width , height ) {
if ( ! this . state . localStream ) {
showNotification ( '本地视频流不可用' , 'error' ) ;
return ;
}
const videoTracks = this . state . localStream . getVideoTracks ( ) ;
if ( videoTracks . length === 0 ) {
showNotification ( '视频轨道不可用' , 'error' ) ;
return ;
}
const track = videoTracks [ 0 ] ;
const label = height >= 1440 ? '2K 1440p' : height >= 1080 ? '1080p 超清' : height >= 720 ? '720p 高清' : '480p 流畅' ;
try {
// 使用 applyConstraints 在不重新获取流的情况下调整分辨率
await track . applyConstraints ( {
width : { ideal : width , max : width } ,
height : { ideal : height , max : height } ,
frameRate : { ideal : 30 , max : 30 }
} ) ;
console . log ( ` 分辨率已切换为 ${ width } x ${ height } ` ) ;
// 根据分辨率调整编码比特率
// 480p: ~1Mbps, 720p: ~2.5Mbps, 1080p: ~4Mbps, 2K: ~6Mbps
const bitrateMap = {
270 : 1000000 , // 480p
720 : 2500000 , // 720p
1080 : 4000000 , // 1080p
1440 : 6000000 // 2K
} ;
const maxBitrate = bitrateMap [ height ] || 2500000 ;
this . _applyMaxBitrate ( maxBitrate ) ;
// 保存当前分辨率设置到本地存储
const userSettings = JSON . parse ( localStorage . getItem ( 'userSettings' ) || '{}' ) ;
userSettings . resolution = { width , height } ;
localStorage . setItem ( 'userSettings' , JSON . stringify ( userSettings ) ) ;
// 通知 UI 更新
this . notify ( { type : 'RESOLUTION_CHANGED' , resolution : { width , height , label } } ) ;
showNotification ( ` 已切换为 ${ label } ` , 'success' ) ;
} catch ( error ) {
console . error ( '切换分辨率失败:' , error ) ;
showNotification ( '切换分辨率失败,摄像头不支持该分辨率' , 'error' ) ;
}
}
/ * *
* 对所有视频 sender 应用 maxBitrate
* @ param { number } maxBitrate - 最大比特率 ( bps )
* /
_applyMaxBitrate ( maxBitrate ) {
if ( ! this . renderstreaming ) return ;
const isHost = this . role === 'host' ;
const participantIds = isHost ? Object . keys ( this . state . participants ) : [ null ] ;
for ( const pid of participantIds ) {
const transceivers = this . renderstreaming . getTransceivers ( pid ) ;
if ( ! transceivers ) continue ;
const videoTransceivers = transceivers . filter ( t =>
t . sender && t . sender . track && t . sender . track . kind === 'video'
) ;
for ( const transceiver of videoTransceivers ) {
try {
const sender = transceiver . sender ;
const params = sender . getParameters ( ) ;
if ( ! params . encodings || params . encodings . length === 0 ) {
params . encodings = [ { } ] ;
}
params . encodings [ 0 ] . maxBitrate = maxBitrate ;
sender . setParameters ( params ) ;
console . log ( ` Updated maxBitrate to ${ maxBitrate } for ${ pid || 'self' } ` ) ;
} catch ( error ) {
console . error ( 'Error updating maxBitrate:' , error ) ;
}
}
}
}
// 更新远端媒体状态 (由 WebSocket 触发)
updateRemoteMedia ( mediaState , participantId ) {
this . state . session . remoteUser . mediaState = {
... this . state . session . remoteUser . mediaState ,
... mediaState
} ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , mediaState , participantId } ) ;
// 通知UI更新用户列表
this . notify ( { type : 'USER_LIST_UPDATE' , localUser : this . state . session . localUser , remoteUser : this . state . session . remoteUser } ) ;
}
// 更新远端用户状态
updateRemoteUserStatus ( status ) {
this . state . session . remoteUser . status = status ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , localUser : this . state . session . localUser , remoteUser : this . state . session . remoteUser } ) ;
}
updateRemoteUserNetworkQuality ( networkQuality ) {
this . state . session . remoteUser . networkQuality = networkQuality ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , localUser : this . state . session . localUser , remoteUser : this . state . session . remoteUser } ) ;
}
// 结束通话(用户主动点击挂断按钮)
async endCall ( ) {
console . log ( ` endCall called. Role: ${ this . role } ` ) ;
// 调用 hangUp() 正确关闭 WebRTC 连接并发送断开信令
// hangUp 内部会根据角色区分:
// - host: 通知所有participants, 删除房间
// - participant: 仅自己离开,房间保留
await this . hangUp ( ) ;
}
// 加入通话
async joinCall ( connectionId ) {
this . state . session . status = 'connecting' ;
this . notify ( { type : 'CALL_STATUS_CHANGE' , status : 'connecting' } ) ;
showNotification ( ` 正在加入通话 ( ${ connectionId } ) ` ) ;
// 初始化
await this . init ( ) ;
// 保存连接ID
this . connectionId = connectionId ;
}
// 创建通话
async createCall ( ) {
this . state . session . status = 'connecting' ;
this . notify ( { type : 'CALL_STATUS_CHANGE' , status : 'connecting' } ) ;
showNotification ( '正在创建通话...' ) ;
// 初始化
await this . init ( ) ;
}
// 真实网络质量检测
async detectNetworkQuality ( ) {
if ( ! this . renderstreaming ) {
return ;
}
try {
const stats = await this . renderstreaming . getStats ( ) ;
if ( ! stats ) {
return ;
}
let totalPacketsLost = 0 ;
let totalPacketsReceived = 0 ;
let inboundRTPCount = 0 ;
let jitter = 0 ;
let roundTripTime = 0 ;
// 分析统计信息
stats . forEach ( report => {
if ( report . type === 'inbound-rtp' && report . mediaType === 'video' ) {
inboundRTPCount ++ ;
// 计算丢包率
if ( report . packetsLost !== undefined && report . packetsReceived !== undefined ) {
totalPacketsLost += report . packetsLost ;
totalPacketsReceived += report . packetsReceived ;
}
// 获取抖动
if ( report . jitter !== undefined ) {
jitter = Math . max ( jitter , report . jitter ) ;
}
// 获取往返时间
if ( report . roundTripTime !== undefined ) {
roundTripTime = Math . max ( roundTripTime , report . roundTripTime ) ;
}
}
} ) ;
// 计算网络质量指标
let quality = 'excellent' ;
if ( inboundRTPCount > 0 ) {
// 基于丢包率判断
const packetLossRate = totalPacketsReceived > 0 ? ( totalPacketsLost / ( totalPacketsLost + totalPacketsReceived ) ) : 0 ;
// 基于抖动判断
const jitterMs = jitter * 1000 ;
// 基于往返时间判断
const rttMs = roundTripTime * 1000 ;
// 综合评估网络质量
if ( packetLossRate > 0.05 || jitterMs > 100 || rttMs > 300 ) {
quality = 'poor' ;
} else if ( packetLossRate > 0.02 || jitterMs > 50 || rttMs > 150 ) {
quality = 'fair' ;
} else if ( packetLossRate > 0.01 || jitterMs > 30 || rttMs > 100 ) {
quality = 'good' ;
} else {
quality = 'excellent' ;
}
} else {
// 没有收到任何RTP包, 设置为无信号状态
quality = 'no_signal' ;
}
// 更新网络质量状态
if ( this . state . session . remoteUser . networkQuality !== quality ) {
this . state . session . remoteUser . networkQuality = quality ;
this . notify ( { type : 'NETWORK_CHANGE' , quality } ) ;
}
} catch ( error ) {
console . error ( 'Error detecting network quality:' , error ) ;
}
}
// 音频活动检测
startActivityDetection ( stream , { isLocal = false } = { } ) {
if ( ! stream ) {
return ;
}
const audioTracks = stream . getAudioTracks ( ) ;
if ( audioTracks . length === 0 ) {
return ;
}
try {
const { threshold , debounceTime , fftSize } = VAD _CONFIG ;
const audioContext = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
const source = audioContext . createMediaStreamSource ( stream ) ;
const analyser = audioContext . createAnalyser ( ) ;
analyser . fftSize = fftSize ;
source . connect ( analyser ) ;
const dataArray = new Uint8Array ( analyser . frequencyBinCount ) ;
let isSpeaking = false ;
let lastActivityTime = 0 ;
const detectActivity = ( ) => {
if ( ! stream || ! this . renderstreaming ) {
return ;
}
analyser . getByteTimeDomainData ( dataArray ) ;
let sum = 0 ;
for ( let i = 0 ; i < dataArray . length ; i ++ ) {
const amplitude = dataArray [ i ] - 128 ;
sum += amplitude * amplitude ;
}
const rms = Math . sqrt ( sum / dataArray . length ) ;
const level = rms / 128 ;
const currentTime = Date . now ( ) ;
if ( level > threshold / 100 ) {
lastActivityTime = currentTime ;
if ( ! isSpeaking ) {
isSpeaking = true ;
if ( isLocal ) {
this . state . session . localUser . mediaState . isSpeaking = true ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'isSpeaking' , value : true } ) ;
this . emitMediaStateChange ( ) ;
} else {
this . updateRemoteMedia ( { isSpeaking : true } ) ;
}
}
} else if ( isSpeaking && currentTime - lastActivityTime > debounceTime ) {
isSpeaking = false ;
if ( isLocal ) {
this . state . session . localUser . mediaState . isSpeaking = false ;
this . notify ( { type : 'LOCAL_MEDIA_CHANGE' , mediaType : 'isSpeaking' , value : false } ) ;
this . emitMediaStateChange ( ) ;
} else {
this . updateRemoteMedia ( { isSpeaking : false } ) ;
}
}
if ( this . state . session . status === 'ongoing' ) {
requestAnimationFrame ( detectActivity ) ;
}
} ;
detectActivity ( ) ;
console . log ( ` ${ isLocal ? 'Local' : 'Remote' } activity detection started ` ) ;
} catch ( error ) {
console . error ( ` Error starting ${ isLocal ? 'local' : 'remote' } activity detection: ` , error ) ;
}
}
// 启动网络质量检测
startNetworkQualityDetection ( ) {
// 每3秒检测一次网络质量
this . networkQualityInterval = setInterval ( ( ) => {
this . detectNetworkQuality ( ) ;
} , 3000 ) ;
}
// 停止网络质量检测
stopNetworkQualityDetection ( ) {
if ( this . networkQualityInterval ) {
clearInterval ( this . networkQualityInterval ) ;
this . networkQualityInterval = null ;
}
}
// 发送媒体状态到服务器
emitMediaStateChange ( ) {
const payload = {
userId : this . state . session . localUser . id ,
... this . state . session . localUser . mediaState
} ;
console . log ( '[WebSocket Emit] media-state-changed:' , payload ) ;
// 使用WebRTC发送媒体状态变化
if ( this . renderstreaming ) {
this . renderstreaming . sendMessage ( {
type : 'media-state-changed' ,
data : payload
} ) ;
}
}
// 显示统计信息
async showStatsMessage ( ) {
console . log ( 'Showing stats message' ) ;
// 立即执行一次网络质量检测
await this . detectNetworkQuality ( ) ;
// 定期显示详细统计信息
this . statsInterval = setInterval ( async ( ) => {
if ( ! this . renderstreaming ) {
return ;
}
try {
const stats = await this . renderstreaming . getStats ( ) ;
if ( ! stats ) {
return ;
}
let statsSummary = {
video : {
packetsLost : 0 ,
packetsReceived : 0 ,
bytesReceived : 0 ,
jitter : 0 ,
roundTripTime : 0 ,
fps : 0 ,
bitrate : 0
} ,
audio : {
packetsLost : 0 ,
packetsReceived : 0 ,
bytesReceived : 0 ,
jitter : 0
}
} ;
// 分析统计信息
stats . forEach ( report => {
if ( report . type === 'inbound-rtp' ) {
if ( report . mediaType === 'video' ) {
statsSummary . video . packetsLost = report . packetsLost || 0 ;
statsSummary . video . packetsReceived = report . packetsReceived || 0 ;
statsSummary . video . bytesReceived = report . bytesReceived || 0 ;
statsSummary . video . jitter = report . jitter || 0 ;
statsSummary . video . roundTripTime = report . roundTripTime || 0 ;
statsSummary . video . fps = report . framesPerSecond || 0 ;
// 计算视频比特率 (kbps)
if ( report . bytesReceived && report . timestamp ) {
const duration = report . timestamp / 1000 ; // 转换为秒
statsSummary . video . bitrate = duration > 0 ? Math . round ( ( report . bytesReceived * 8 ) / ( duration * 1000 ) ) : 0 ;
}
} else if ( report . mediaType === 'audio' ) {
statsSummary . audio . packetsLost = report . packetsLost || 0 ;
statsSummary . audio . packetsReceived = report . packetsReceived || 0 ;
statsSummary . audio . bytesReceived = report . bytesReceived || 0 ;
statsSummary . audio . jitter = report . jitter || 0 ;
}
}
} ) ;
// 输出详细统计信息
console . log ( '=== WebRTC Statistics ===' ) ;
console . log ( ` Network Quality: ${ this . state . session . remoteUser . networkQuality } ` ) ;
console . log ( 'Video Stats:' , {
'Packets Lost' : statsSummary . video . packetsLost ,
'Packets Received' : statsSummary . video . packetsReceived ,
'Packet Loss Rate' : statsSummary . video . packetsReceived > 0 ?
` ${ ( ( statsSummary . video . packetsLost / ( statsSummary . video . packetsLost + statsSummary . video . packetsReceived ) ) * 100 ) . toFixed ( 2 ) } % ` : '0%' ,
'Jitter' : ` ${ ( statsSummary . video . jitter * 1000 ) . toFixed ( 2 ) } ms ` ,
'Round Trip Time' : ` ${ ( statsSummary . video . roundTripTime * 1000 ) . toFixed ( 2 ) } ms ` ,
'FPS' : statsSummary . video . fps . toFixed ( 1 ) ,
'Bitrate' : ` ${ statsSummary . video . bitrate } kbps `
} ) ;
console . log ( 'Audio Stats:' , {
'Packets Lost' : statsSummary . audio . packetsLost ,
'Packets Received' : statsSummary . audio . packetsReceived ,
'Packet Loss Rate' : statsSummary . audio . packetsReceived > 0 ?
` ${ ( ( statsSummary . audio . packetsLost / ( statsSummary . audio . packetsLost + statsSummary . audio . packetsReceived ) ) * 100 ) . toFixed ( 2 ) } % ` : '0%' ,
'Jitter' : ` ${ ( statsSummary . audio . jitter * 1000 ) . toFixed ( 2 ) } ms `
} ) ;
console . log ( '========================' ) ;
} catch ( error ) {
console . error ( 'Error showing stats message:' , error ) ;
}
} , 5000 ) ; // 每5秒更新一次统计信息
}
// 清除统计信息
clearStatsMessage ( ) {
console . log ( 'Clearing stats message' ) ;
// 清理统计信息定时器
if ( this . statsInterval ) {
clearInterval ( this . statsInterval ) ;
this . statsInterval = null ;
}
}
// Getters
getState ( ) { return this . state ; }
getLocalUser ( ) { return this . state . session . localUser ; }
getRemoteUser ( ) { return this . state . session . remoteUser ; }
getConnectionId ( ) { return this . connectionId ; }
getRenderStreaming ( ) { return this . renderstreaming ; }
}
// 创建单例实例
const store = new CallStateManager ( ) ;
// 页面卸载前清理
window . addEventListener ( 'beforeunload' , async ( ) => {
if ( ! store . renderstreaming )
return ;
await store . renderstreaming . stop ( ) ; // 停止WebRTC连接
} , true ) ;
export default store ;