AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Service/Calling/AzureCommunicationCalling/CallingSDKEventsHandler.swift (424 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import AzureCommunicationCalling import Foundation import Combine // swiftlint:disable file_length class CallingSDKEventsHandler: NSObject, CallingSDKEventsHandling { var participantsInfoListSubject: CurrentValueSubject<[ParticipantInfoModel], Never> = .init([]) var callInfoSubject = PassthroughSubject<CallInfoModel, Never>() var isRecordingActiveSubject = PassthroughSubject<Bool, Never>() var isTranscriptionActiveSubject = PassthroughSubject<Bool, Never>() var dominantSpeakersSubject: CurrentValueSubject<[String], Never> = .init([]) var isLocalUserMutedSubject = PassthroughSubject<Bool, Never>() var callIdSubject = PassthroughSubject<String, Never>() var participantRoleSubject = PassthroughSubject<ParticipantRoleEnum, Never>() var totalParticipantCountSubject = PassthroughSubject<Int, Never>() /* <CALL_START_TIME> var callStartTimeSubject = PassthroughSubject<Date, Never>() </CALL_START_TIME> */ var capabilitiesChangedSubject = PassthroughSubject<CapabilitiesChangedEvent, Never>() var captionsSupportedSpokenLanguages = CurrentValueSubject<[String], Never>([]) var captionsSupportedCaptionLanguages = CurrentValueSubject<[String], Never>([]) var isCaptionsTranslationSupported = CurrentValueSubject<Bool, Never>(false) var captionsReceived = PassthroughSubject<CallCompositeCaptionsData, Never>() var rttReceived = PassthroughSubject<CallCompositeRttData, Never>() var activeSpokenLanguageChanged = CurrentValueSubject<String, Never>("") var activeCaptionLanguageChanged = CurrentValueSubject<String, Never>("") var captionsEnabledChanged = CurrentValueSubject<Bool, Never>(false) var captionsTypeChanged = CurrentValueSubject<CallCompositeCaptionsType, Never>(.none) // User Facing Diagnostics Subjects var networkQualityDiagnosticsSubject = PassthroughSubject<NetworkQualityDiagnosticModel, Never>() var networkDiagnosticsSubject = PassthroughSubject<NetworkDiagnosticModel, Never>() var mediaDiagnosticsSubject = PassthroughSubject<MediaDiagnosticModel, Never>() private let logger: Logger private var remoteParticipantEventAdapter = RemoteParticipantsEventsAdapter() private var recordingCallFeature: RecordingCallFeature? private var transcriptionCallFeature: TranscriptionCallFeature? private var dominantSpeakersCallFeature: DominantSpeakersCallFeature? private var localUserDiagnosticsFeature: LocalUserDiagnosticsCallFeature? private var captionsFeature: CaptionsCallFeature? private var teamsCaptions: TeamsCaptions? private var communicationCaptions: CommunicationCaptions? private var capabilitiesCallFeature: CapabilitiesCallFeature? private var realTimeTextCallFeature: RealTimeTextCallFeature? private var previousCallingStatus: CallingStatus = .none private var remoteParticipants = MappedSequence<String, AzureCommunicationCalling.RemoteParticipant>() private let communicationCaptionsHandler = CommunicationCaptionsHandler() private let teamsCaptionsHandler = TeamsCaptionsHandler() init(logger: Logger) { self.logger = logger super.init() setupRemoteParticipantEventsAdapter() communicationCaptionsHandler.parentHandler = self teamsCaptionsHandler.parentHandler = self } func assign(_ recordingCallFeature: RecordingCallFeature) { self.recordingCallFeature = recordingCallFeature recordingCallFeature.delegate = self } func assign(_ transcriptionCallFeature: TranscriptionCallFeature) { self.transcriptionCallFeature = transcriptionCallFeature transcriptionCallFeature.delegate = self } func assign(_ dominantSpeakersCallFeature: DominantSpeakersCallFeature) { self.dominantSpeakersCallFeature = dominantSpeakersCallFeature dominantSpeakersCallFeature.delegate = self } func assign(_ localUserDiagnosticsFeature: LocalUserDiagnosticsCallFeature) { self.localUserDiagnosticsFeature = localUserDiagnosticsFeature localUserDiagnosticsFeature.mediaDiagnostics.delegate = self localUserDiagnosticsFeature.networkDiagnostics.delegate = self } func assign(_ captionsFeature: CaptionsCallFeature) { self.captionsFeature = captionsFeature captionsFeature.delegate = self } func assign(_ capabilitiesCallFeature: CapabilitiesCallFeature) { self.capabilitiesCallFeature = capabilitiesCallFeature self.capabilitiesCallFeature?.delegate = self } func assign(_ realTimeTextCallFeature: RealTimeTextCallFeature) { self.realTimeTextCallFeature = realTimeTextCallFeature realTimeTextCallFeature.delegate = self } func setupProperties() { participantsInfoListSubject.value.removeAll() recordingCallFeature = nil transcriptionCallFeature = nil dominantSpeakersCallFeature = nil localUserDiagnosticsFeature = nil captionsFeature = nil realTimeTextCallFeature = nil remoteParticipants = MappedSequence<String, AzureCommunicationCalling.RemoteParticipant>() previousCallingStatus = .none capabilitiesCallFeature = nil } private func setupRemoteParticipantEventsAdapter() { let participantUpdate: ((AzureCommunicationCalling.RemoteParticipant) -> Void) = { [weak self] remoteParticipant in guard let self = self else { return } let userIdentifier = remoteParticipant.identifier.rawId self.updateRemoteParticipant(userIdentifier: userIdentifier) } remoteParticipantEventAdapter.onIsMutedChanged = participantUpdate remoteParticipantEventAdapter.onVideoStreamsUpdated = participantUpdate remoteParticipantEventAdapter.onStateChanged = participantUpdate remoteParticipantEventAdapter.onDominantSpeakersChanged = participantUpdate remoteParticipantEventAdapter.onIsSpeakingChanged = { [weak self] remoteParticipant in guard let self = self else { return } let userIdentifier = remoteParticipant.identifier.rawId _ = remoteParticipant.isSpeaking self.updateRemoteParticipant(userIdentifier: userIdentifier) } } private func removeRemoteParticipants( _ remoteParticipants: [AzureCommunicationCalling.RemoteParticipant] ) { for participant in remoteParticipants { let userIdentifier = participant.identifier.rawId self.remoteParticipants.removeValue(forKey: userIdentifier)?.delegate = nil } removeRemoteParticipantsInfoModel(remoteParticipants) } private func removeRemoteParticipantsInfoModel( _ remoteParticipants: [AzureCommunicationCalling.RemoteParticipant] ) { guard !remoteParticipants.isEmpty else { return } var remoteParticipantsInfoList = participantsInfoListSubject.value remoteParticipantsInfoList = remoteParticipantsInfoList.filter { infoModel in !remoteParticipants.contains(where: { $0.identifier.rawId == infoModel.userIdentifier }) } participantsInfoListSubject.send(remoteParticipantsInfoList) } private func addRemoteParticipants( _ remoteParticipants: [AzureCommunicationCalling.RemoteParticipant] ) { var remoteParticipantsInfoList = participantsInfoListSubject.value for participant in remoteParticipants { let userIdentifier = participant.identifier.rawId if self.remoteParticipants.value(forKey: userIdentifier) == nil { participant.delegate = remoteParticipantEventAdapter self.remoteParticipants.append(forKey: userIdentifier, value: participant) let infoModel = participant.toParticipantInfoModel() remoteParticipantsInfoList.append(infoModel) } } participantsInfoListSubject.send(remoteParticipantsInfoList) } private func updateRemoteParticipant(userIdentifier: String) { var remoteParticipantsInfoList = participantsInfoListSubject.value if let remoteParticipant = remoteParticipants.value(forKey: userIdentifier), let index = remoteParticipantsInfoList.firstIndex(where: { $0.userIdentifier == userIdentifier }) { let newInfoModel = remoteParticipant.toParticipantInfoModel() remoteParticipantsInfoList[index] = newInfoModel participantsInfoListSubject.send(remoteParticipantsInfoList) } } private func wasCallConnected() -> Bool { return previousCallingStatus == .connected || previousCallingStatus == .localHold || previousCallingStatus == .remoteHold } } extension CallingSDKEventsHandler: CallDelegate, RecordingCallFeatureDelegate, TranscriptionCallFeatureDelegate, DominantSpeakersCallFeatureDelegate, MediaDiagnosticsDelegate, NetworkDiagnosticsDelegate, CaptionsCallFeatureDelegate, RealTimeTextCallFeatureDelegate, CapabilitiesCallFeatureDelegate { func call(_ call: Call, didChangeId args: PropertyChangedEventArgs) { callIdSubject.send(call.id) } func call(_ call: Call, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) { if !args.removedParticipants.isEmpty { removeRemoteParticipants(args.removedParticipants) } if !args.addedParticipants.isEmpty { addRemoteParticipants(args.addedParticipants) } } /* <CALL_START_TIME> func call(_ call: Call, didUpdateStartTime args: PropertyChangedEventArgs) { callStartTimeSubject.send(call.startTime) } </CALL_START_TIME> */ func call(_ call: Call, didChangeState args: PropertyChangedEventArgs) { onStateChanged(call: call) } func onStateChanged(call: Call) { callIdSubject.send(call.id) let currentStatus = call.state.toCallingStatus() let internalError = call.callEndReason.toCompositeInternalError(wasCallConnected()) if internalError != nil { let code = call.callEndReason.code let subcode = call.callEndReason.subcode logger.error("Receive vaildate CallEndReason:\(code), subcode:\(subcode)") } if currentStatus == .connected { self.captionsFeature = call.feature(Features.captions) self.captionsFeature?.getCaptions {(value, error) in if let error = error { self.logger.error("Can not get the captions with error:\(error)") } else { if value?.type == CaptionsType.communicationCaptions { // communication captions self.communicationCaptions = value as? CommunicationCaptions self.communicationCaptions?.delegate = self.communicationCaptionsHandler self.captionsSupportedSpokenLanguages.send(self.communicationCaptions? .supportedSpokenLanguages ?? []) self.captionsTypeChanged.send(.communication) } if value?.type == CaptionsType.teamsCaptions { // teams captions self.teamsCaptions = value as? TeamsCaptions self.teamsCaptions?.delegate = self.teamsCaptionsHandler self.captionsSupportedSpokenLanguages.send(self.teamsCaptions?.supportedSpokenLanguages ?? []) self.captionsSupportedCaptionLanguages.send(self.teamsCaptions?.supportedCaptionLanguages ?? []) self.captionsTypeChanged.send(.teams) } } } } let callInfoModel = CallInfoModel(status: currentStatus, internalError: internalError, callEndReasonCode: Int(call.callEndReason.code), callEndReasonSubCode: Int(call.callEndReason.subcode)) logger.debug( "callInfoModel \(callInfoModel.status)") logger.debug( "remoteParticipants \(call.remoteParticipants.count)") callInfoSubject.send(callInfoModel) if currentStatus == .connected || currentStatus == .connecting { addRemoteParticipants(call.remoteParticipants) } if currentStatus == .disconnected { call.delegate = nil } self.previousCallingStatus = currentStatus } func recordingCallFeature(_ recordingCallFeature: RecordingCallFeature, didChangeRecordingState args: PropertyChangedEventArgs) { let newRecordingActive = recordingCallFeature.isRecordingActive isRecordingActiveSubject.send(newRecordingActive) } func transcriptionCallFeature(_ transcriptionCallFeature: TranscriptionCallFeature, didChangeTranscriptionState args: PropertyChangedEventArgs) { let newTranscriptionActive = transcriptionCallFeature.isTranscriptionActive isTranscriptionActiveSubject.send(newTranscriptionActive) } func realTimeTextCallFeature(_ realTextCallFeature: RealTimeTextCallFeature, didReceiveInfo args: RealTimeTextInfoReceivedEventArgs) { let rttMessage = args.info.toCallCompositeRttData() if let index = participantsInfoListSubject.value.firstIndex(where: { $0.userIdentifier == rttMessage.senderRawId }) { // Update the participant in place var updatedList = participantsInfoListSubject.value updatedList[index].isTypingRtt = rttMessage.resultType != .final && !rttMessage.text.isEmpty self.participantsInfoListSubject.send(updatedList) } rttReceived.send(rttMessage) } func dominantSpeakersCallFeature(_ dominantSpeakersCallFeature: DominantSpeakersCallFeature, didChangeDominantSpeakers args: PropertyChangedEventArgs) { let dominantSpeakersInfo = dominantSpeakersCallFeature.dominantSpeakersInfo var speakers = [String]() for speaker in dominantSpeakersInfo.speakers { let userIdentifier = speaker.rawId speakers.append(userIdentifier) } dominantSpeakersSubject.send(speakers) } func call(_ call: Call, didChangeMuteState args: PropertyChangedEventArgs) { isLocalUserMutedSubject.send(call.isOutgoingAudioMuted) } func call(_ call: Call, didChangeRole args: PropertyChangedEventArgs) { let role = call.callParticipantRole.toParticipantRole() participantRoleSubject.send(role) } func call(_ call: Call, didChangeTotalParticipantCount args: PropertyChangedEventArgs) { // substract local participant from total participantCount totalParticipantCountSubject.send(Int(call.totalParticipantCount) - 1) } // MARK: CapabilitiesDelegate func capabilitiesCallFeature(_ capabilitiesCallFeature: CapabilitiesCallFeature, didChangeCapabilities args: CapabilitiesChangedEventArgs) { let capabilitiesChangedEvent = args.toCapabilitiesChangedEvent() self.capabilitiesChangedSubject.send(capabilitiesChangedEvent) } // MARK: NetworkDiagnosticsDelegate func networkDiagnostics(_ networkDiagnostics: NetworkDiagnostics, didChangeNetworkSendQuality args: DiagnosticQualityChangedEventArgs) { let model = NetworkQualityDiagnosticModel( diagnostic: .networkSendQuality, value: args.value.toCallCompositeDiagnosticQuality() ) self.networkQualityDiagnosticsSubject.send(model) } func networkDiagnostics(_ networkDiagnostics: NetworkDiagnostics, didChangeNetworkReconnectionQuality args: DiagnosticQualityChangedEventArgs) { let model = NetworkQualityDiagnosticModel( diagnostic: .networkReconnectionQuality, value: args.value.toCallCompositeDiagnosticQuality() ) self.networkQualityDiagnosticsSubject.send(model) } func networkDiagnostics(_ networkDiagnostics: NetworkDiagnostics, didChangeNetworkReceiveQuality args: DiagnosticQualityChangedEventArgs) { let model = NetworkQualityDiagnosticModel( diagnostic: .networkReceiveQuality, value: args.value.toCallCompositeDiagnosticQuality() ) self.networkQualityDiagnosticsSubject.send(model) } func networkDiagnostics(_ networkDiagnostics: NetworkDiagnostics, didChangeIsNetworkUnavailable args: DiagnosticFlagChangedEventArgs) { let model = NetworkDiagnosticModel(diagnostic: .networkUnavailable, value: args.value) self.networkDiagnosticsSubject.send(model) } func networkDiagnostics(_ networkDiagnostics: NetworkDiagnostics, didChangeIsNetworkRelaysUnreachable args: DiagnosticFlagChangedEventArgs) { let model = NetworkDiagnosticModel(diagnostic: .networkRelaysUnreachable, value: args.value) self.networkDiagnosticsSubject.send(model) } // MARK: MediaDiagnosticsDelegate func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsSpeakerBusy args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .speakerBusy, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsCameraFrozen args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .cameraFrozen, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsSpeakerMuted args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .speakerMuted, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsMicrophoneBusy args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .microphoneBusy, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsCameraStartFailed args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .cameraStartFailed, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsSpeakerVolumeZero args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .speakerVolumeZero, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsSpeakerNotFunctioning args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .speakerNotFunctioning, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsCameraPermissionDenied args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .cameraPermissionDenied, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsMicrophoneNotFunctioning args: DiagnosticFlagChangedEventArgs) { // .microphoneNotFunctioning is unhandled for now because there is a false positive // event from SDK that is fixed, but pending release. } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsCameraStartTimedOut args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .cameraStartTimedOut, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsMicrophoneMutedUnexpectedly args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .microphoneMutedUnexpectedly, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsNoSpeakerDevicesAvailable args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .noSpeakerDevicesAvailable, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsNoMicrophoneDevicesAvailable args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .noMicrophoneDevicesAvailable, value: args.value) self.mediaDiagnosticsSubject.send(model) } func mediaDiagnostics(_ mediaDiagnostics: MediaDiagnostics, didChangeIsSpeakingWhileMicrophoneIsMuted args: DiagnosticFlagChangedEventArgs) { let model = MediaDiagnosticModel(diagnostic: .speakingWhileMicrophoneIsMuted, value: args.value) self.mediaDiagnosticsSubject.send(model) } } private class CommunicationCaptionsHandler: NSObject, CommunicationCaptionsDelegate { weak var parentHandler: CallingSDKEventsHandler? func communicationCaptions(_ communicationCaptions: CommunicationCaptions, didReceiveCaptions: CommunicationCaptionsReceivedEventArgs) { parentHandler?.captionsReceived.send(didReceiveCaptions.toCallCompositeCaptionsData()) } func communicationCaptions(_ communicationCaptions: CommunicationCaptions, didChangeActiveSpokenLanguageState args: PropertyChangedEventArgs) { let spokenLanguage = communicationCaptions.activeSpokenLanguage parentHandler?.activeSpokenLanguageChanged.send(spokenLanguage) } func communicationCaptions(_ communicationCaptions: CommunicationCaptions, didChangeCaptionsEnabledState args: PropertyChangedEventArgs) { let isCaptionsEnabled = communicationCaptions.isEnabled parentHandler?.captionsEnabledChanged.send(isCaptionsEnabled) } } private class TeamsCaptionsHandler: NSObject, TeamsCaptionsDelegate { weak var parentHandler: CallingSDKEventsHandler? func teamsCaptions(_ teamsCaptions: TeamsCaptions, didChangeCaptionsEnabledState args: PropertyChangedEventArgs) { parentHandler?.captionsEnabledChanged.send(teamsCaptions.isEnabled) } func teamsCaptions(_ teamsCaptions: TeamsCaptions, didChangeActiveSpokenLanguageState args: PropertyChangedEventArgs) { let spokenLanguage = teamsCaptions.activeSpokenLanguage parentHandler?.activeSpokenLanguageChanged.send(spokenLanguage) } func teamsCaptions(_ teamsCaptions: TeamsCaptions, didReceiveCaptions args: TeamsCaptionsReceivedEventArgs) { parentHandler?.captionsReceived.send(args.toCallCompositeCaptionsData()) } func teamsCaptions(_ teamsCaptions: TeamsCaptions, didChangeActiveCaptionLanguageState args: PropertyChangedEventArgs) { let captionsLanguage = teamsCaptions.activeCaptionLanguage parentHandler?.activeCaptionLanguageChanged.send(captionsLanguage) } }