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' ;
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 = [ ] ;
}
// 订阅状态变化
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 ( ) ;
console . log ( 'Signaling connected (WebSocket only, no room yet)' ) ;
return this . _signaling ;
}
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 } ` ) ;
if ( ! this . state . participants [ participantId ] ) {
this . state . participants [ participantId ] = {
id : '' ,
name : '参与者' ,
avatar : '/images/p2.png' ,
mediaState : { audio : false , video : true , isSpeaking : false } ,
status : 'online'
} ;
}
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
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 的用户信息
delete this . state . participants [ participantId ] ;
// 通知UI更新, 用participantId作为connectionId传给renderer
this . notify ( { type : 'PARTICIPANT_LEFT' , connectionId : participantId } ) ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
this . broadcastParticipantsList ( ) ;
} ;
// 轨道事件回调
this . renderstreaming . onTrackEvent = ( data ) => {
const direction = data . transceiver . direction ;
if ( direction == "sendrecv" || direction == "recvonly" ) {
// 使用participantId区分不同participant的流
const trackParticipantId = data . participantId || this . connectionId ;
const isHost = this . role === 'host' ;
let targetStream = null ;
if ( isHost ) {
// Host端: 按 participantId 管理多路远端流
if ( ! this . state . remoteStreams [ trackParticipantId ] ) {
this . state . remoteStreams [ trackParticipantId ] = new MediaStream ( ) ;
}
targetStream = this . state . remoteStreams [ trackParticipantId ] ;
} else {
// Participant端: 使用单一远端流
if ( this . state . remoteStream == null ) {
this . state . remoteStream = new MediaStream ( ) ;
}
targetStream = this . state . remoteStream ;
}
// 检查是否已经有相同类型的轨道
const existingTracks = targetStream . getTracks ( ) . filter ( track => track . kind === data . track . kind ) ;
existingTracks . forEach ( track => {
targetStream . removeTrack ( track ) ;
console . log ( 'Removed old track:' , track . kind ) ;
} ) ;
targetStream . addTrack ( data . track ) ;
console . log ( 'Added new track:' , data . track . kind , 'for participant:' , trackParticipantId ) ;
// Host端兜底: 确保participants中有该participant条目
if ( isHost && ! this . state . participants [ trackParticipantId ] ) {
this . state . participants [ trackParticipantId ] = {
id : '' ,
name : '参与者' ,
avatar : '/images/p2.png' ,
mediaState : { audio : false , video : true , isSpeaking : false } ,
status : 'online'
} ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
this . broadcastParticipantsList ( ) ;
}
// 通知UI远程流已更新
// 关键优化:如果是音频轨道先到达且流中尚无视频轨道,
// 延迟通知UI等待视频轨道到达, 避免音频先触发的UI更新导致黑屏
const notifyStreamUpdate = ( ) => {
this . notify ( {
type : 'REMOTE_STREAM_OBTAINED' ,
stream : targetStream ,
connectionId : trackParticipantId ,
isHost : isHost
} ) ;
console . log ( 'Notified UI about remote stream update' ) ;
} ;
if ( data . track . kind === 'audio' && targetStream . getVideoTracks ( ) . length === 0 ) {
// 音频先到, 视频尚未到达: 延迟200ms通知, 给视频轨道到达的机会
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 ) ;
} else {
// 视频轨道到达,或音频视频同时存在:立即通知
notifyStreamUpdate ( ) ;
}
// 只有当收到远程流时才更新远程用户状态为在线
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
} ) ;
// 启动通话时长计时器(避免重复启动)
if ( ! this . durationInterval ) {
this . durationInterval = setInterval ( ( ) => {
this . state . session . duration ++ ;
this . notify ( { type : 'DURATION_UPDATE' , duration : this . state . session . duration } ) ;
} , 1000 ) ;
}
}
// 如果是音频轨道,启动远程音频活动检测
if ( data . track . kind === 'audio' ) {
this . startActivityDetection ( this . state . remoteStream , { isLocal : false } ) ;
}
} else if ( direction == "sendonly" ) {
// 本地发送轨道,启动本地音频活动检测
if ( data . track . kind === 'audio' ) {
this . startActivityDetection ( this . state . localStream , { isLocal : true } ) ;
}
}
} ;
this . renderstreaming . onMessage = ( data ) => {
console . log ( '收到消息:' , data ) ;
if ( data . type === 'chat-message' ) {
// 处理聊天
// 添加到列表并更新UI
chatMessage . handleChatMessage ( data . message ) ;
// Host端: 按participantId更新对应用户信息
if ( data . participantId && this . role === 'host' && this . state . participants [ data . participantId ] ) {
this . state . participants [ data . participantId ] . id = data . message . senderId ;
if ( data . message . senderName ) {
this . state . participants [ data . participantId ] . name = data . message . senderName ;
}
if ( data . message . senderAvatar ) {
this . state . participants [ data . participantId ] . avatar = data . message . senderAvatar ;
}
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
this . broadcastParticipantsList ( ) ;
}
// Participant端: 根据消息来源更新对应用户信息
if ( ! this . role || this . role !== 'host' ) {
if ( data . participantId && this . state . participants [ data . participantId ] ) {
// 来自其他Participant的消息: 更新participants中对应条目
if ( data . message . senderName ) {
this . state . participants [ data . participantId ] . name = data . message . senderName ;
}
if ( data . message . senderAvatar ) {
this . state . participants [ data . participantId ] . avatar = data . message . senderAvatar ;
}
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
} else if ( data . message && data . message . senderId !== this . state . session . localUser . id ) {
// 来自Host的消息: 更新remoteUser
this . state . session . remoteUser = {
... this . state . session . remoteUser ,
id : data . message . senderId ,
name : data . message . senderName ,
avatar : data . message . senderAvatar
} ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , mediaState : this . state . session . remoteUser . mediaState } ) ;
}
}
} else if ( data . type === 'media-state-changed' ) {
// 处理媒体状态变化
console . log ( '收到媒体状态变化:' , data . data , 'from participant:' , data . participantId ) ;
if ( this . role === 'host' ) {
// Host端: 按participantId同步更新participants中对应participant的mediaState
if ( data . participantId && this . state . participants [ data . participantId ] ) {
this . state . participants [ data . participantId ] . mediaState = {
... this . state . participants [ data . participantId ] . mediaState ,
... data . data
} ;
}
// 更新远端媒体状态
this . updateRemoteMedia ( data . data , data . participantId ) ;
// 通知UI更新participants
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
// Host端广播最新成员列表( 含媒体状态) 给所有Participant
this . broadcastParticipantsList ( ) ;
} else {
// Participant端: 根据消息来源更新对应条目
2026-05-16 20:11:36 +08:00
// Host的participantId在participants-sync中也会同步, 所以不能仅靠participants中有无该key判断
// 自身发出的消息回声( participantId === selfParticipantId) 可以忽略
// 来自其他Participant: participantId存在且在participants中, 且不是自身
// 来自Host: participantId存在但不是自身( Host不在selfParticipantId中)
if ( data . participantId && data . participantId !== this . selfParticipantId && this . state . participants [ data . participantId ] ) {
2026-04-29 15:18:30 +08:00
// 来自其他Participant的媒体状态变化: 仅更新participants中对应条目
// 不调用updateRemoteMedia, 因为Participant端没有其他Participant的视频流
this . state . participants [ data . participantId ] . mediaState = {
... this . state . participants [ data . participantId ] . mediaState ,
... data . data
} ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
2026-05-16 20:11:36 +08:00
} else if ( data . participantId === this . selfParticipantId ) {
// 自身消息回声,忽略
} else {
// 来自Host的媒体状态变化( Host的participantId不匹配participants中任何条目, 或无participantId) :
// 更新remoteUser( Host的视频流是本端远端画面)
console . log ( 'Received media-state-changed from Host, updating remoteUser:' , data . data ) ;
2026-04-29 15:18:30 +08:00
this . updateRemoteMedia ( data . data , data . participantId ) ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
}
}
} else if ( data . type === 'user-info' ) {
// 处理用户信息更新
console . log ( '收到用户信息:' , data . data , 'from participant:' , data . participantId ) ;
if ( data . data ) {
if ( data . participantId && this . role === 'host' ) {
// Host端: 按participantId存储到participants Map
if ( ! this . state . participants [ data . participantId ] ) {
this . state . participants [ data . participantId ] = {
id : '' ,
name : '参与者' ,
avatar : '/images/p2.png' ,
mediaState : { audio : false , video : true , isSpeaking : false } ,
status : 'online'
} ;
}
this . state . participants [ data . participantId ] . id = data . data . id || '' ;
this . state . participants [ data . participantId ] . name = data . data . name || '参与者' ;
this . state . participants [ data . participantId ] . avatar = data . data . avatar || '/images/p2.png' ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
this . broadcastParticipantsList ( ) ;
} else {
// Participant端: 更新单一remoteUser( Host的信息)
this . state . session . remoteUser = {
... this . state . session . remoteUser ,
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
} ;
this . notify ( { type : 'REMOTE_MEDIA_CHANGE' , mediaState : this . state . session . remoteUser . mediaState } ) ;
}
}
} else if ( data . type === 'participants-sync' ) {
// Participant端: 接收Host广播的完整成员列表
if ( this . role !== 'host' && data . data ) {
console . log ( '收到成员列表同步:' , data . data ) ;
// 过滤掉自身条目, 避免在列表中重复显示( 自身已作为localUser显示)
const filtered = { } ;
for ( const [ pid , pInfo ] of Object . entries ( data . data ) ) {
if ( pid !== this . selfParticipantId ) {
filtered [ pid ] = pInfo ;
}
}
this . state . participants = filtered ;
this . notify ( { type : 'PARTICIPANTS_UPDATE' , participants : this . state . participants } ) ;
// 同步通话时长: 仅首次同步, 将Host的时长作为基准
if ( ! this . durationSynced && typeof data . callDuration === 'number' ) {
this . state . session . duration = data . callDuration ;
this . durationSynced = true ;
// 如果计时器尚未启动(远程流还未到达),先启动计时器
if ( ! this . durationInterval ) {
this . durationInterval = setInterval ( ( ) => {
this . state . session . duration ++ ;
this . notify ( { type : 'DURATION_UPDATE' , duration : this . state . session . duration } ) ;
} , 1000 ) ;
}
this . notify ( { type : 'DURATION_UPDATE' , duration : this . state . session . duration } ) ;
console . log ( ` 通话时长已同步,当前时长: ${ data . callDuration } 秒 ` ) ;
}
}
}
} ;
}
/ * *
* 启动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' } ) ;
}
/ * *
* 发送消息
* @ param { string } type - 消息类型
* @ param { Object } data - 消息数据
* /
sendMessage ( type , data ) {
if ( this . renderstreaming ) {
this . renderstreaming . sendMessage ( {
type : type ,
data : data
} ) ;
}
}
/ * *
* Host端广播完整成员列表给所有Participant
* 包含Host自身信息 + 所有Participant信息
* Participant收到后可展示完整通话成员列表
* /
broadcastParticipantsList ( ) {
if ( this . role !== 'host' || ! this . renderstreaming ) return ;
const memberList = { } ;
// 添加Host自身信息
memberList [ 'host' ] = {
id : this . state . session . localUser . id ,
name : this . state . session . localUser . name ,
avatar : this . state . session . localUser . avatar ,
mediaState : { ... this . state . session . localUser . mediaState } ,
status : 'online' ,
role : 'host'
} ;
// 添加所有Participant信息
for ( const [ pid , pInfo ] of Object . entries ( this . state . participants ) ) {
memberList [ pid ] = {
... pInfo ,
role : 'participant'
} ;
}
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 ;