diff --git a/client/public/store.js b/client/public/store.js index 5b74918..67160e9 100644 --- a/client/public/store.js +++ b/client/public/store.js @@ -159,150 +159,191 @@ class CallStateManager { // 更新本地媒体状态 async updateLocalMedia(mediaType, value) { + await this._updateLocalMediaRefactored(mediaType, value); + return; + } - // 如果是开启视频,重新获取摄像头资源 + async _updateLocalMediaRefactored(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(); + await this._enableLocalVideo(); + this._notifyUserListUpdate(); + return; } + this.state.session.localUser.mediaState[mediaType] = value; + this._notifyLocalMediaChange(mediaType, value); + this.emitMediaStateChange(); - // 如果是关闭视频,释放摄像头资源 - if (mediaType === 'video' && !value && this.state.localStream) { + if (mediaType === 'video' && !value) { + this._disableLocalVideoTracks(); + } + + if (mediaType === 'audio') { + this._setLocalAudioTrackEnabled(value); + } + + this._notifyUserListUpdate(); + } + + async _enableLocalVideo() { + try { + const newVideoTrack = await this._requestNewVideoTrack(); + this._replaceLocalVideoTrack(newVideoTrack); + await this._updateOutgoingVideoTrack(newVideoTrack); + + this.state.session.localUser.mediaState.video = true; + this.notify({ type: 'LOCAL_STREAM_OBTAINED', stream: this.state.localStream }); + this._notifyLocalMediaChange('video', 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.state.localStream.getTracks().forEach(track => { - if (track.kind === 'video') { - track.stop(); - } - }); - // 发送媒体状态到服务器 - this.emitMediaStateChange(); + this._notifyLocalMediaChange('video', false); + } + } + async _requestNewVideoTrack() { + 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 (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; - } + return newVideoTrack; + } + + _replaceLocalVideoTrack(newVideoTrack) { + if (this.state.localStream) { + const oldVideoTracks = this.state.localStream.getVideoTracks(); + oldVideoTracks.forEach(track => { + track.stop(); + this.state.localStream.removeTrack(track); }); - // 发送媒体状态到服务器 - this.emitMediaStateChange(); + this.state.localStream.addTrack(newVideoTrack); + return; } - // 通知UI更新用户列表 - this.notify({ type: 'USER_LIST_UPDATE', localUser: this.state.session.localUser, remoteUser: this.state.session.remoteUser }); + this.state.localStream = new MediaStream([newVideoTrack]); + } + async _updateOutgoingVideoTrack(newVideoTrack) { + if (!this.renderstreaming) { + return; + } + + console.log('Updating video track in WebRTC connection'); + + if (this.role === 'host') { + const participantIds = Object.keys(this.state.remoteStreams); + for (const participantId of participantIds) { + await this._updateVideoTrackForPeer(newVideoTrack, participantId); + } + return; + } + + await this._updateVideoTrackForPeer(newVideoTrack); + } + + async _updateVideoTrackForPeer(newVideoTrack, participantId = undefined) { + const transceivers = this.renderstreaming.getTransceivers(participantId); + if (!transceivers) { + return; + } + + const videoTransceivers = transceivers.filter(transceiver => + transceiver.sender && transceiver.sender.track && transceiver.sender.track.kind === 'video' + ); + + if (videoTransceivers.length > 0) { + await this._replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId); + } else { + this._addVideoTransceiver(newVideoTrack, participantId); + } + + this._scheduleVideoSenderUpdate(participantId); + } + + async _replaceVideoTrackOnTransceivers(videoTransceivers, newVideoTrack, participantId) { + for (const transceiver of videoTransceivers) { + try { + await transceiver.sender.replaceTrack(newVideoTrack); + console.log(participantId + ? `Replaced video track for participant ${participantId}` + : 'Successfully replaced video track'); + } catch (error) { + console.error( + participantId + ? `Error replacing video track for ${participantId}:` + : 'Error replacing video track:', + error + ); + } + } + } + + _addVideoTransceiver(newVideoTrack, participantId) { + try { + if (participantId) { + this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }, participantId); + console.log(`Added new video transceiver for participant ${participantId}`); + return; + } + + this.renderstreaming.addTransceiver(newVideoTrack, { direction: 'sendonly' }); + console.log('Added new video transceiver'); + } catch (error) { + console.error( + participantId + ? `Error adding video transceiver for ${participantId}:` + : 'Error adding video transceiver:', + error + ); + } + } + + _scheduleVideoSenderUpdate(participantId) { + setTimeout(() => { this.setCodecPreferences(participantId); }, 100); + setTimeout(() => { this.setVideoEncodingParameters(participantId); }, 200); + } + + _disableLocalVideoTracks() { + if (!this.state.localStream) { + return; + } + + this.state.session.localUser.mediaState.video = false; + this.state.localStream.getTracks().forEach(track => { + if (track.kind === 'video') { + track.stop(); + } + }); + } + + _setLocalAudioTrackEnabled(value) { + if (!this.state.localStream) { + return; + } + + this.state.session.localUser.mediaState.audio = value; + this.state.localStream.getTracks().forEach(track => { + if (track.kind === 'audio') { + track.enabled = value; + } + }); + } + + _notifyLocalMediaChange(mediaType, value) { + this.notify({ type: 'LOCAL_MEDIA_CHANGE', mediaType, value }); + } + + _notifyUserListUpdate() { + this.notify({ + type: 'USER_LIST_UPDATE', + localUser: this.state.session.localUser, + remoteUser: this.state.session.remoteUser + }); } /**