Compare commits

1 Commits

3 changed files with 141 additions and 14 deletions

View File

@@ -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 // 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/(.*)$": "<rootDir>/src/$1"
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [], // modulePathIgnorePatterns: [],

View File

@@ -13,6 +13,19 @@ import { MeetingRecorder } from './media/meeting-recorder.js';
import { ServerRecordingPeer } from './media/server-recording-peer.js'; import { ServerRecordingPeer } from './media/server-recording-peer.js';
const logger = createLogger('store'); 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 { class CallStateManager {
constructor() { constructor() {
this.state = { this.state = {
@@ -932,29 +945,34 @@ class CallStateManager {
_handleMediaStateChangedMessage(data) { _handleMediaStateChangedMessage(data) {
logger.debug('收到媒体状态更新:', data.data, 'from participant:', data.participantId); logger.debug('收到媒体状态更新:', data.data, 'from participant:', data.participantId);
if (this.role === 'host') { if (this.role === 'host') {
if (data.participantId && this.state.participants[data.participantId]) { const participantChanged = this._updateParticipantMediaStateIfChanged(data.participantId, data.data);
this._upsertParticipant(data.participantId, { const remoteChanged = this._updateRemoteMediaIfChanged(data.data, data.participantId);
mediaState: data.data if (participantChanged) {
}); this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
}
if (!participantChanged && !remoteChanged) {
logger.debug('媒体状态未变化,跳过更新:', data.participantId);
} }
this.updateRemoteMedia(data.data, data.participantId);
this._notifyParticipantsUpdate();
this.broadcastParticipantsList();
return; return;
} }
if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) { if (data.participantId && data.participantId !== this.selfParticipantId && this.state.participants[data.participantId]) {
this._upsertParticipant(data.participantId, { if (this._updateParticipantMediaStateIfChanged(data.participantId, data.data)) {
mediaState: data.data this._notifyParticipantsUpdate();
}); } else {
this._notifyParticipantsUpdate(); logger.debug('媒体状态未变化,跳过参与者更新:', data.participantId);
}
return; return;
} }
if (data.participantId === this.selfParticipantId) { if (data.participantId === this.selfParticipantId) {
return; return;
} }
logger.debug('Received media-state-changed from Host, updating remoteUser:', data.data); logger.debug('Received media-state-changed from Host, updating remoteUser:', data.data);
this.updateRemoteMedia(data.data, data.participantId); if (this._updateRemoteMediaIfChanged(data.data, data.participantId)) {
this._notifyParticipantsUpdate(); this._notifyParticipantsUpdate();
} else {
logger.debug('媒体状态未变化,跳过远端用户更新:', data.participantId);
}
} }
_handleUserInfoMessage(data) { _handleUserInfoMessage(data) {
logger.debug('收到用户信息:', data.data, 'from participant:', data.participantId); logger.debug('收到用户信息:', data.data, 'from participant:', data.participantId);
@@ -1046,6 +1064,25 @@ class CallStateManager {
_upsertParticipant(participantId, patch = {}) { _upsertParticipant(participantId, patch = {}) {
return upsertParticipant(this.state.participants, 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) { _removeParticipant(participantId) {
return removeParticipant(this.state.participants, participantId); return removeParticipant(this.state.participants, participantId);
} }

View File

@@ -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'
}));
});
});