From 37f195b48c6f47cc3d2e978af9d57772dc7f1ed2 Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.com> Date: Wed, 3 Jun 2026 21:00:15 +0800 Subject: [PATCH] =?UTF-8?q?media-state-changed=20=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=A6=82=E6=9E=9C=E7=8A=B6=E6=80=81=E6=B2=A1=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E7=9A=84=E8=AF=9D=EF=BC=8C=E4=B8=8D=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/jest.config.js | 4 +- client/public/call/store.js | 63 ++++++++++++---- client/test/unit/store-media-state.test.js | 88 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 client/test/unit/store-media-state.test.js diff --git a/client/jest.config.js b/client/jest.config.js index 7a52a86..b3bce00 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -80,7 +80,9 @@ export default { ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + moduleNameMapper: { + "^/module/(.*)$": "/src/$1" + }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], diff --git a/client/public/call/store.js b/client/public/call/store.js index 89adf3c..024cdbe 100644 --- a/client/public/call/store.js +++ b/client/public/call/store.js @@ -13,6 +13,19 @@ import { MeetingRecorder } from './media/meeting-recorder.js'; import { ServerRecordingPeer } from './media/server-recording-peer.js'; const logger = createLogger('store'); +const MEDIA_STATE_KEYS = ['audio', 'video', 'screenShare', 'recording', 'isSpeaking']; + +function hasMediaStateChanged(current = {}, next = {}) { + if (!next || typeof next !== 'object') { + return false; + } + + return MEDIA_STATE_KEYS.some(key => ( + Object.prototype.hasOwnProperty.call(next, key) + && Boolean(current?.[key]) !== Boolean(next[key]) + )); +} + class CallStateManager { constructor() { this.state = { @@ -932,29 +945,34 @@ class CallStateManager { _handleMediaStateChangedMessage(data) { logger.debug('收到媒体状态更新:', 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 - }); + const participantChanged = this._updateParticipantMediaStateIfChanged(data.participantId, data.data); + const remoteChanged = this._updateRemoteMediaIfChanged(data.data, data.participantId); + if (participantChanged) { + this._notifyParticipantsUpdate(); + this.broadcastParticipantsList(); + } + if (!participantChanged && !remoteChanged) { + logger.debug('媒体状态未变化,跳过更新:', data.participantId); } - 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(); + if (this._updateParticipantMediaStateIfChanged(data.participantId, data.data)) { + this._notifyParticipantsUpdate(); + } else { + logger.debug('媒体状态未变化,跳过参与者更新:', data.participantId); + } return; } if (data.participantId === this.selfParticipantId) { return; } logger.debug('Received media-state-changed from Host, updating remoteUser:', data.data); - this.updateRemoteMedia(data.data, data.participantId); - this._notifyParticipantsUpdate(); + if (this._updateRemoteMediaIfChanged(data.data, data.participantId)) { + this._notifyParticipantsUpdate(); + } else { + logger.debug('媒体状态未变化,跳过远端用户更新:', data.participantId); + } } _handleUserInfoMessage(data) { logger.debug('收到用户信息:', data.data, 'from participant:', data.participantId); @@ -1046,6 +1064,25 @@ class CallStateManager { _upsertParticipant(participantId, patch = {}) { return upsertParticipant(this.state.participants, participantId, patch); } + _updateParticipantMediaStateIfChanged(participantId, mediaState) { + if (!participantId || !this.state.participants[participantId]) { + return false; + } + if (!hasMediaStateChanged(this.state.participants[participantId].mediaState, mediaState)) { + return false; + } + this._upsertParticipant(participantId, { + mediaState + }); + return true; + } + _updateRemoteMediaIfChanged(mediaState, participantId) { + if (!hasMediaStateChanged(this.state.session.remoteUser.mediaState, mediaState)) { + return false; + } + this.updateRemoteMedia(mediaState, participantId); + return true; + } _removeParticipant(participantId) { return removeParticipant(this.state.participants, participantId); } diff --git a/client/test/unit/store-media-state.test.js b/client/test/unit/store-media-state.test.js new file mode 100644 index 0000000..353ebbc --- /dev/null +++ b/client/test/unit/store-media-state.test.js @@ -0,0 +1,88 @@ +import { jest } from '@jest/globals'; + +const { default: store } = await import('../../public/call/store.js'); + +function mediaState(overrides = {}) { + return { + audio: true, + video: true, + screenShare: false, + recording: false, + isSpeaking: false, + ...overrides + }; +} + +function resetStore() { + store.role = 'host'; + store.selfParticipantId = 'host'; + store.renderstreaming = { + sendMessage: jest.fn() + }; + store.state = { + session: { + duration: 0, + localUser: { + id: 'host-user', + name: 'Host', + avatar: '/images/p1.png', + mediaState: mediaState() + }, + remoteUser: { + id: 'remote-user', + name: 'Remote', + avatar: '/images/p2.png', + status: 'online', + mediaState: mediaState() + } + }, + participants: { + 'participant-1': { + id: 'participant-user', + name: 'Participant', + avatar: '/images/p2.png', + mediaState: mediaState(), + status: 'online' + } + } + }; + store.notify = jest.fn(); +} + +describe('media-state-changed handling', () => { + beforeEach(() => { + resetStore(); + }); + + test('skips updates when participant media state is unchanged', () => { + store._handleMediaStateChangedMessage({ + participantId: 'participant-1', + data: { + userId: 'participant-user', + ...mediaState() + } + }); + + expect(store.notify).not.toHaveBeenCalled(); + expect(store.renderstreaming.sendMessage).not.toHaveBeenCalled(); + }); + + test('updates and broadcasts when participant media state changes', () => { + store._handleMediaStateChangedMessage({ + participantId: 'participant-1', + data: { + userId: 'participant-user', + ...mediaState({ audio: false }) + } + }); + + expect(store.state.participants['participant-1'].mediaState.audio).toBe(false); + expect(store.notify).toHaveBeenCalledWith({ + type: 'PARTICIPANTS_UPDATE', + participants: store.state.participants + }); + expect(store.renderstreaming.sendMessage).toHaveBeenCalledWith(expect.objectContaining({ + type: 'participants-sync' + })); + }); +});