@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
247 lines • 16.2 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { Features } from '@azure/communication-calling';
import { toFlatCommunicationIdentifier } from "../../acs-ui-common/src";
import { CaptionsFeatureSubscriber } from './CaptionsSubscriber';
import { convertSdkLocalStreamToDeclarativeLocalStream, convertSdkParticipantToDeclarativeParticipant } from './Converter';
import { LocalVideoStreamVideoEffectsSubscriber } from './LocalVideoStreamVideoEffectsSubscriber';
import { ParticipantSubscriber } from './ParticipantSubscriber';
import { RecordingSubscriber } from './RecordingSubscriber';
import { PPTLiveSubscriber } from './PPTLiveSubscriber';
import { disposeView } from './StreamUtils';
import { TranscriptionSubscriber } from './TranscriptionSubscriber';
import { UserFacingDiagnosticsSubscriber } from './UserFacingDiagnosticsSubscriber';
import { RaiseHandSubscriber } from './RaiseHandSubscriber';
import { OptimalVideoCountSubscriber } from './OptimalVideoCountSubscriber';
import { CapabilitiesSubscriber } from './CapabilitiesSubscriber';
import { ReactionSubscriber } from './ReactionSubscriber';
import { SpotlightSubscriber } from './SpotlightSubscriber';
import { BreakoutRoomsSubscriber } from './BreakoutRoomsSubscriber';
import { TogetherModeSubscriber } from './TogetherModeSubscriber';
import { MediaAccessSubscriber } from './MediaAccessSubscriber';
import { _isTeamsMeeting } from './TypeGuards';
import { RealTimeTextSubscriber } from './RealTimeTextSubscriber';
/**
* Keeps track of the listeners assigned to a particular call because when we get an event from SDK, it doesn't tell us
* which call it is for. If we keep track of this then we know which call in the state that needs an update and also
* which property of that call. Also we can use this when unregistering to a call.
*/
export class CallSubscriber {
constructor(call, context, internalContext) {
this._safeSubscribeInitCaptionSubscriber = () => {
this._safeSubscribe(this.initCaptionSubscriber);
};
this._safeSubscribeInitTeamsMeetingConference = () => {
this._safeSubscribe(this.initTeamsMeetingConference);
};
this._safeSubscribeInitRealTimeText = () => {
this._safeSubscribe(this.initRealTimeText);
};
this.subscribe = () => {
this._call.on('stateChanged', this.stateChanged);
this._call.on('stateChanged', this._safeSubscribeInitCaptionSubscriber);
this._call.on('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
this._call.on('idChanged', this.idChanged);
this._call.on('isScreenSharingOnChanged', this.isScreenSharingOnChanged);
this._call.on('remoteParticipantsUpdated', this.remoteParticipantsUpdated);
this._call.on('localVideoStreamsUpdated', this.localVideoStreamsUpdated);
this._call.on('isMutedChanged', this.isMuteChanged);
this._call.on('roleChanged', this.callRoleChangedHandler);
this._call.feature(Features.DominantSpeakers).on('dominantSpeakersChanged', this.dominantSpeakersChanged);
this._call.on('mutedByOthers', this.mutedByOthersHandler);
this._safeSubscribeInitRealTimeText();
for (const localVideoStream of this._call.localVideoStreams) {
this._internalContext.setLocalRenderInfo(this._callIdRef.callId, localVideoStream.mediaStreamType, localVideoStream, 'NotRendered', undefined);
}
if (this._call.remoteParticipants.length > 0) {
this._call.remoteParticipants.forEach((participant) => {
this.addParticipantListener(participant);
});
this._context.setCallRemoteParticipants(this._callIdRef.callId, this._call.remoteParticipants.map(convertSdkParticipantToDeclarativeParticipant), []);
}
};
this.unsubscribe = () => {
var _a, _b, _c;
this._call.off('stateChanged', this.stateChanged);
this._call.off('stateChanged', this._safeSubscribeInitCaptionSubscriber);
this._call.off('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
this._call.off('idChanged', this.idChanged);
this._call.off('isScreenSharingOnChanged', this.isScreenSharingOnChanged);
this._call.off('remoteParticipantsUpdated', this.remoteParticipantsUpdated);
this._call.off('localVideoStreamsUpdated', this.localVideoStreamsUpdated);
this._call.off('isMutedChanged', this.isMuteChanged);
this._call.off('roleChanged', this.callRoleChangedHandler);
this._call.off('mutedByOthers', this.mutedByOthersHandler);
this._participantSubscribers.forEach((participantSubscriber) => {
participantSubscriber.unsubscribe();
});
this._participantSubscribers.clear();
// If we are unsubscribing that means we no longer want to display any video for this call (callEnded or callAgent
// disposed) and we should not be updating it any more. So if video is rendering we stop rendering.
for (const localVideoStream of this._call.localVideoStreams) {
const mediaStreamType = localVideoStream.mediaStreamType;
disposeView(this._context, this._internalContext, this._callIdRef.callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localVideoStream));
this._internalContext.deleteLocalRenderInfo(this._callIdRef.callId, mediaStreamType);
}
this._diagnosticsSubscriber.unsubscribe();
this._recordingSubscriber.unsubscribe();
this._transcriptionSubscriber.unsubscribe();
this._optimalVideoCountSubscriber.unsubscribe();
this._pptLiveSubscriber.unsubscribe();
(_a = this._CaptionsFeatureSubscriber) === null || _a === void 0 ? void 0 : _a.unsubscribe();
if (this._realTimeTextSubscriber) {
this._realTimeTextSubscriber.unsubscribe();
}
(_b = this._raiseHandSubscriber) === null || _b === void 0 ? void 0 : _b.unsubscribe();
this._capabilitiesSubscriber.unsubscribe();
(_c = this._reactionSubscriber) === null || _c === void 0 ? void 0 : _c.unsubscribe();
this._spotlightSubscriber.unsubscribe();
this._breakoutRoomsSubscriber.unsubscribe();
this._togetherModeSubscriber.unsubscribe();
this._mediaAccessSubscriber.unsubscribe();
};
this.stateChanged = () => {
this._context.setCallState(this._callIdRef.callId, this._call.state);
};
this.initCaptionSubscriber = () => {
// subscribe to captions here so that we don't call captions when call is not initialized
if (this._call.state === 'Connected' && !this._CaptionsFeatureSubscriber) {
this._CaptionsFeatureSubscriber = new CaptionsFeatureSubscriber(this._callIdRef, this._context, this._call.feature(Features.Captions));
this._call.off('stateChanged', this._safeSubscribeInitCaptionSubscriber);
}
};
this.initTeamsMeetingConference = () => {
if (this._call.state === 'Connected' && _isTeamsMeeting(this._call)) {
this._call.feature(Features.TeamsMeetingAudioConferencing).getTeamsMeetingAudioConferencingDetails().then(teamsMeetingConferenceDetails => {
this._context.setTeamsMeetingConference(this._callIdRef.callId, teamsMeetingConferenceDetails);
});
this._call.off('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
}
};
this.initRealTimeText = () => {
if (this._context.getState().userId.kind !== 'microsoftTeamsUser') {
this._realTimeTextSubscriber = new RealTimeTextSubscriber(this._callIdRef, this._context, this._call.feature(Features.RealTimeText));
}
};
this.idChanged = () => {
this._internalContext.setCallId(this._call.id, this._callIdRef.callId);
this._context.setCallId(this._call.id, this._callIdRef.callId);
this._callIdRef.callId = this._call.id;
};
this.isScreenSharingOnChanged = () => {
this._context.setCallIsScreenSharingOn(this._callIdRef.callId, this._call.isScreenSharingOn);
};
this.isMuteChanged = () => {
this._context.setCallIsMicrophoneMuted(this._callIdRef.callId, this._call.isMuted);
};
this.callRoleChangedHandler = () => {
this._context.setRole(this._callIdRef.callId, this._call.role);
};
// TODO: Tee to notification state once available
this.mutedByOthersHandler = () => {
this._context.teeErrorToState({
name: 'mutedByOthers',
message: 'Muted by another participant'
}, 'Call.mutedByOthers');
};
this.remoteParticipantsUpdated = (event) => {
event.added.forEach((participant) => {
this.addParticipantListener(participant);
});
event.removed.forEach((participant) => {
this.removeParticipantListener(participant);
});
// Remove any added participants from remoteParticipantsEnded if they are there and add any removed participants to
// remoteParticipantsEnded.
this._context.setCallRemoteParticipantsEnded(this._callIdRef.callId, event.removed.map(convertSdkParticipantToDeclarativeParticipant), event.added.map((participant) => {
return toFlatCommunicationIdentifier(participant.identifier);
}));
// Add added participants to remoteParticipants and remove removed participants from remoteParticipants.
this._context.setCallRemoteParticipants(this._callIdRef.callId, event.added.map(convertSdkParticipantToDeclarativeParticipant), event.removed.map((participant) => {
return toFlatCommunicationIdentifier(participant.identifier);
}));
};
this.localVideoStreamsUpdated = (event) => {
var _a, _b;
for (const localVideoStream of event.added) {
const mediaStreamType = localVideoStream.mediaStreamType;
// IMPORTANT: The internalContext should be set before context. This is done to ensure that the internal context
// has the required data when component re-renders due to external state changes.
this._internalContext.setLocalRenderInfo(this._callIdRef.callId, mediaStreamType, localVideoStream, 'NotRendered', undefined);
// Subscribe to video effect changes
(_a = this._localVideoStreamVideoEffectsSubscribers.get(mediaStreamType)) === null || _a === void 0 ? void 0 : _a.unsubscribe();
this._localVideoStreamVideoEffectsSubscribers.set(mediaStreamType, new LocalVideoStreamVideoEffectsSubscriber({
parent: this._callIdRef,
context: this._context,
localVideoStream: localVideoStream,
localVideoStreamEffectsAPI: localVideoStream.feature(Features.VideoEffects)
}));
}
for (const localVideoStream of event.removed) {
const mediaStreamType = localVideoStream.mediaStreamType;
(_b = this._localVideoStreamVideoEffectsSubscribers.get(mediaStreamType)) === null || _b === void 0 ? void 0 : _b.unsubscribe();
disposeView(this._context, this._internalContext, this._callIdRef.callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localVideoStream));
this._internalContext.deleteLocalRenderInfo(this._callIdRef.callId, mediaStreamType);
}
this._context.setCallLocalVideoStream(this._callIdRef.callId, event.added.map(convertSdkLocalStreamToDeclarativeLocalStream), event.removed.map(convertSdkLocalStreamToDeclarativeLocalStream));
};
this.dominantSpeakersChanged = () => {
const dominantSpeakers = this._call.feature(Features.DominantSpeakers).dominantSpeakers;
this._context.setCallDominantSpeakers(this._callIdRef.callId, dominantSpeakers);
};
this._call = call;
this._callIdRef = {
callId: call.id
};
this._context = context;
this._internalContext = internalContext;
this._diagnosticsSubscriber = new UserFacingDiagnosticsSubscriber(this._callIdRef, this._context, this._call.feature(Features.UserFacingDiagnostics));
this._participantSubscribers = new Map();
this._recordingSubscriber = new RecordingSubscriber(this._callIdRef, this._context, this._call.feature(Features.Recording));
this._pptLiveSubscriber = new PPTLiveSubscriber(this._callIdRef, this._context, this._call);
this._transcriptionSubscriber = new TranscriptionSubscriber(this._callIdRef, this._context, this._call.feature(Features.Transcription));
this._raiseHandSubscriber = new RaiseHandSubscriber(this._callIdRef, this._context, this._call.feature(Features.RaiseHand));
this._reactionSubscriber = new ReactionSubscriber(this._callIdRef, this._context, this._call.feature(Features.Reaction));
this._optimalVideoCountSubscriber = new OptimalVideoCountSubscriber({
callIdRef: this._callIdRef,
context: this._context,
localOptimalVideoCountFeature: this._call.feature(Features.OptimalVideoCount)
});
this._localVideoStreamVideoEffectsSubscribers = new Map();
this._capabilitiesSubscriber = new CapabilitiesSubscriber(this._callIdRef, this._context, this._call.feature(Features.Capabilities));
this._spotlightSubscriber = new SpotlightSubscriber(this._callIdRef, this._context, this._call.feature(Features.Spotlight));
// Clear assigned breakout room closed notification for this call.
this._context.deleteLatestNotification('assignedBreakoutRoomClosed');
this._breakoutRoomsSubscriber = new BreakoutRoomsSubscriber(this._callIdRef, this._context, this._call.feature(Features.BreakoutRooms));
this._togetherModeSubscriber = new TogetherModeSubscriber(this._callIdRef, this._context, this._internalContext, this._call.feature(Features.TogetherMode));
this._mediaAccessSubscriber = new MediaAccessSubscriber(this._callIdRef, this._context, this._call.feature(Features.MediaAccess));
this.subscribe();
}
// This is a helper function to safely call subscriber functions. This is needed in order to prevent events
// with the same event type from failing to fire due to a subscriber throwing an error upon subscription.
// Wrap your listeners with this helper function if your listener can fail due to initialization or fail
// during a function call. This will prevent other events using the same event type from failing to fire.
_safeSubscribe(subscriber) {
try {
subscriber();
}
catch (e) {
this._context.teeErrorToState(e, 'Call.on');
}
}
addParticipantListener(participant) {
var _a;
const participantKey = toFlatCommunicationIdentifier(participant.identifier);
(_a = this._participantSubscribers.get(participantKey)) === null || _a === void 0 ? void 0 : _a.unsubscribe();
this._participantSubscribers.set(participantKey, new ParticipantSubscriber(this._callIdRef, participant, this._context, this._internalContext));
}
removeParticipantListener(participant) {
const participantKey = toFlatCommunicationIdentifier(participant.identifier);
const participantSubscriber = this._participantSubscribers.get(participantKey);
if (participantSubscriber) {
participantSubscriber.unsubscribe();
this._participantSubscribers.delete(participantKey);
}
}
}
//# sourceMappingURL=CallSubscriber.js.map