AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Redux/Middleware/CallingMiddlewareHandler.swift (778 lines of code) (raw):
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
import Combine
import Foundation
// swiftlint:disable file_length
protocol CallingMiddlewareHandling {
@discardableResult
func setupCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func startCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func endCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func holdCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func resumeCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func recordingStateUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
isRecordingActive: Bool) -> Task<Void, Never>
@discardableResult
func transcriptionStateUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
isTranscriptionActive: Bool) -> Task<Void, Never>
@discardableResult
func enterBackground(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func enterForeground(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func willTerminate(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func audioSessionInterrupted(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestCameraPreviewOn(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestCameraOn(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestCameraOff(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestCameraSwitch(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestMicrophoneMute(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func requestMicrophoneUnmute(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func onCameraPermissionIsSet(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func onMicPermissionIsGranted(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func admitAllLobbyParticipants(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func declineAllLobbyParticipants(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func admitLobbyParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never>
@discardableResult
func declineLobbyParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never>
@discardableResult
func startCaptions(state: AppState,
dispatch: @escaping ActionDispatch,
language: String) -> Task<Void, Never>
@discardableResult
func stopCaptions(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func sendRttMessage(message: String, isFinal: Bool) -> Task<Void, Never>
@discardableResult
func setCaptionsSpokenLanguage(state: AppState,
dispatch: @escaping ActionDispatch,
language: String) -> Task<Void, Never>
@discardableResult
func setCaptionsLanguage(state: AppState,
dispatch: @escaping ActionDispatch,
language: String) -> Task<Void, Never>
func setCapabilities(capabilities: Set<ParticipantCapabilityType>,
state: AppState,
dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func onCapabilitiesChanged(event: CapabilitiesChangedEvent,
state: AppState,
dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func onNetworkQualityCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: NetworkQualityDiagnosticModel) -> Task<Void, Never>
@discardableResult
func onNetworkCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: NetworkDiagnosticModel) -> Task<Void, Never>
@discardableResult
func onMediaCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: MediaDiagnosticModel) -> Task<Void, Never>
@discardableResult
func dismissNotification(state: AppState,
dispatch: @escaping ActionDispatch) -> Task<Void, Never>
@discardableResult
func removeParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never>
}
// swiftlint:disable type_body_length
class CallingMiddlewareHandler: CallingMiddlewareHandling {
private let callingService: CallingServiceProtocol
private let logger: Logger
private let cancelBag = CancelBag()
private let subscription = CancelBag()
private let capabilitiesManager: CapabilitiesManager
private let callType: CompositeCallType
init(callingService: CallingServiceProtocol,
logger: Logger,
callType: CompositeCallType,
capabilitiesManager: CapabilitiesManager) {
self.callingService = callingService
self.logger = logger
self.callType = callType
self.capabilitiesManager = capabilitiesManager
}
func setupCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.setupCall()
if state.defaultUserState.cameraState == .on,
state.errorState.internalError == nil {
await requestCameraPreviewOn(state: state, dispatch: dispatch).value
}
if state.defaultUserState.audioState == .on {
dispatch(.localUserAction(.microphonePreviewOn))
}
if state.callingState.operationStatus == .skipSetupRequested {
dispatch(.callingAction(.callStartRequested))
}
} catch {
handle(error: error, errorType: .callJoinFailed, dispatch: dispatch)
}
}
}
func startCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.startCall(
isCameraPreferred: state.localUserState.cameraState.operation == .on,
isAudioPreferred: state.localUserState.audioState.operation == .on
)
subscription(dispatch: dispatch,
isSkipRequested: state.callingState.operationStatus == .skipSetupRequested)
} catch {
handle(error: error, errorType: .callJoinFailed, dispatch: dispatch)
}
}
}
func endCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.endCall()
dispatch(.callingAction(.callEnded))
} catch {
handle(error: error, errorType: .callEndFailed, dispatch: dispatch)
dispatch(.callingAction(.requestFailed))
}
}
}
func holdCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
do {
try await callingService.holdCall()
await requestCameraPause(state: state, dispatch: dispatch).value
} catch {
handle(error: error, errorType: .callHoldFailed, dispatch: dispatch)
}
}
}
func resumeCall(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .localHold else {
return
}
do {
try await callingService.resumeCall()
if state.localUserState.cameraState.operation == .paused {
await requestCameraOn(state: state, dispatch: dispatch).value
}
} catch {
handle(error: error, errorType: .callResumeFailed, dispatch: dispatch)
}
}
}
func recordingStateUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
isRecordingActive: Bool) -> Task<Void, Never> {
Task {
var recordingState: RecordingStatus = .off
if isRecordingActive {
recordingState = .on
} else {
if state.callingState.recordingStatus == .on {
recordingState = .stopped
}
}
dispatch(.callingAction(.recordingUpdated(recordingStatus: recordingState)))
if isRecordingActive {
dispatch(.callingAction(.dismissRecordingTranscriptionBannedUpdated(isDismissed: false)))
}
if isRecordingActive && !state.callingState.isTranscriptionActive {
if state.callingState.transcriptionStatus != .off {
dispatch(.callingAction(.transcriptionUpdated(transcriptionStatus: .off)))
}
}
}
}
func transcriptionStateUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
isTranscriptionActive: Bool) -> Task<Void, Never> {
Task {
var transcriptiongState: RecordingStatus = .off
if isTranscriptionActive {
transcriptiongState = .on
} else {
if state.callingState.transcriptionStatus == .on {
transcriptiongState = .stopped
}
}
dispatch(.callingAction(.transcriptionUpdated(transcriptionStatus: transcriptiongState)))
if isTranscriptionActive {
dispatch(.callingAction(.dismissRecordingTranscriptionBannedUpdated(isDismissed: false)))
}
if isTranscriptionActive && !state.callingState.isRecordingActive {
if state.callingState.recordingStatus != .off {
dispatch(.callingAction(.recordingUpdated(recordingStatus: .off)))
}
}
}
}
func enterBackground(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.lifeCycleState.currentStatus == .foreground else {
return
}
await requestCameraPause(state: state, dispatch: dispatch).value
}
}
func enterForeground(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.lifeCycleState.currentStatus == .background,
state.callingState.status == .connected,
state.localUserState.cameraState.operation == .paused else {
return
}
await requestCameraOn(state: state, dispatch: dispatch).value
}
}
func willTerminate(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
dispatch(.callingAction(.callEndRequested))
}
}
func requestCameraPreviewOn(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
if state.permissionState.cameraPermission == .notAsked {
dispatch(.permissionAction(.cameraPermissionRequested))
} else if state.permissionState.cameraPermission == .denied {
dispatch(.localUserAction(.cameraOffTriggered))
} else {
do {
let identifier = try await callingService.requestCameraPreviewOn()
dispatch(.localUserAction(.cameraOnSucceeded(videoStreamIdentifier: identifier)))
} catch {
dispatch(.localUserAction(.cameraOnFailed(error: error)))
}
}
}
}
func requestCameraOn(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
if state.permissionState.cameraPermission == .notAsked {
dispatch(.permissionAction(.cameraPermissionRequested))
} else {
do {
let streamId = try await callingService.startLocalVideoStream()
dispatch(.localUserAction(.cameraOnSucceeded(videoStreamIdentifier: streamId)))
} catch {
dispatch(.localUserAction(.cameraOnFailed(error: error)))
}
}
}
}
func requestCameraOff(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.stopLocalVideoStream()
dispatch(.localUserAction(.cameraOffSucceeded))
} catch {
dispatch(.localUserAction(.cameraOffFailed(error: error)))
}
}
}
func requestCameraPause(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected,
state.localUserState.cameraState.operation == .on else {
return
}
do {
try await callingService.stopLocalVideoStream()
dispatch(.localUserAction(.cameraPausedSucceeded))
} catch {
dispatch(.localUserAction(.cameraPausedFailed(error: error)))
}
}
}
func requestCameraSwitch(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
let currentCamera = state.localUserState.cameraState.device
do {
let device = try await callingService.switchCamera()
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
dispatch(.localUserAction(.cameraSwitchSucceeded(cameraDevice: device)))
} catch {
dispatch(.localUserAction(.cameraSwitchFailed(previousCamera: currentCamera, error: error)))
}
}
}
func requestMicrophoneMute(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.muteLocalMic()
} catch {
dispatch(.localUserAction(.microphoneOffFailed(error: error)))
}
}
}
func requestMicrophoneUnmute(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.unmuteLocalMic()
} catch {
dispatch(.localUserAction(.microphoneOnFailed(error: error)))
}
}
}
func onCameraPermissionIsSet(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.permissionState.cameraPermission == .requesting else {
return
}
switch state.localUserState.cameraState.transmission {
case .local:
if state.navigationState.status == .inCall {
dispatch(.localUserAction(.cameraOnTriggered))
} else {
dispatch(.localUserAction(.cameraPreviewOnTriggered))
}
case .remote:
dispatch(.localUserAction(.cameraOnTriggered))
}
}
}
func onMicPermissionIsGranted(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.permissionState.audioPermission == .requesting else {
return
}
_ = setupCall(state: state, dispatch: dispatch)
}
}
func audioSessionInterrupted(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
dispatch(.callingAction(.holdRequested))
}
}
func admitAllLobbyParticipants(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
do {
try await callingService.admitAllLobbyParticipants()
} catch {
let errorCode = LobbyErrorCode.convertToLobbyErrorCode(error as NSError)
dispatch(.remoteParticipantsAction(.lobbyError(errorCode: errorCode)))
}
}
}
func declineAllLobbyParticipants(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
let participantIds = state.remoteParticipantsState.participantInfoList.filter { participant in
participant.status == .inLobby
}.map { participant in
participant.userIdentifier
}
for participantId in participantIds {
do {
try await callingService.declineLobbyParticipant(participantId)
} catch {
let errorCode = LobbyErrorCode.convertToLobbyErrorCode(error as NSError)
dispatch(.remoteParticipantsAction(.lobbyError(errorCode: errorCode)))
}
}
}
}
func admitLobbyParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
do {
try await callingService.admitLobbyParticipant(participantId)
} catch {
let errorCode = LobbyErrorCode.convertToLobbyErrorCode(error as NSError)
dispatch(.remoteParticipantsAction(.lobbyError(errorCode: errorCode)))
}
}
}
func declineLobbyParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
do {
try await callingService.declineLobbyParticipant(participantId)
} catch {
let errorCode = LobbyErrorCode.convertToLobbyErrorCode(error as NSError)
dispatch(.remoteParticipantsAction(.lobbyError(errorCode: errorCode)))
}
}
}
func setCapabilities(capabilities: Set<ParticipantCapabilityType>,
state: AppState,
dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status != .disconnected else {
return
}
do {
if !capabilitiesManager.hasCapability(capabilities: capabilities,
capability: ParticipantCapabilityType.turnVideoOn) &&
state.localUserState.cameraState.operation != .off {
dispatch(.localUserAction(.cameraOffTriggered))
}
if !capabilitiesManager.hasCapability(capabilities: capabilities,
capability: ParticipantCapabilityType.unmuteMicrophone) &&
state.localUserState.audioState.operation != .off {
dispatch(.localUserAction(.microphoneOffTriggered))
}
}
}
}
func startCaptions(state: AppState, dispatch: @escaping ActionDispatch, language: String) -> Task<Void, Never> {
Task {
guard state.captionsState.isStarted == false else {
return
}
do {
try await callingService.startCaptions(language)
dispatch(.captionsAction(.started))
} catch {
dispatch(.captionsAction(.error(errors: .captionsFailedToStart)))
dispatch(.captionsAction(.stopped))
if let errorCode = error as NSError? {
switch errorCode.localizedDescription {
case CallCompositeCaptionsErrorsDescription.captionsStartFailedCallNotConnected.rawValue:
dispatch(.errorAction(
.fatalErrorUpdated(internalError: .micNotAvailable, error: nil)))
case CallCompositeCaptionsErrorsDescription.captionsStartFailedSpokenLanguageNotSupported.rawValue:
dispatch(.errorAction(.fatalErrorUpdated(internalError:
.captionsStartFailedSpokenLanguageNotSupported, error: nil)))
default:
return
}
}
}
}
}
func sendRttMessage(message: String, isFinal: Bool) -> Task<Void, Never> {
Task {
do {
try await callingService.sendRttMessage(message, isFinal: isFinal)
} catch {
self.logger.error("Send Rtt message Failed with error : \(error)")
}
}
}
func stopCaptions(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
do {
try await callingService.stopCaptions()
dispatch(.captionsAction(.stopped))
} catch {
dispatch(.captionsAction(.error(errors: .captionsFailedToStop)))
}
}
}
func setCaptionsSpokenLanguage(state: AppState, dispatch: @escaping ActionDispatch, language: String)
-> Task<Void, Never> {
Task {
do {
try await callingService.setCaptionsSpokenLanguage(language)
dispatch(.captionsAction(.spokenLanguageChanged(language: language)))
} catch {
dispatch(.captionsAction(.error(errors: .captionsFailedToSetSpokenLanguage)))
}
}
}
func setCaptionsLanguage(state: AppState, dispatch: @escaping ActionDispatch, language: String)
-> Task<Void, Never> {
Task {
do {
try await callingService.setCaptionsCaptionLanguage(language)
dispatch(.captionsAction(.captionLanguageChanged(language: language)))
} catch {
dispatch(.captionsAction(.error(errors: .captionsFailedToSetCaptionLanguage)))
if let errorCode = error as NSError? {
switch error.localizedDescription {
case CallCompositeCaptionsErrorsDescription.captionsStartFailedCallNotConnected.rawValue:
handle(error: errorCode, errorType: .captionsStartFailedCallNotConnected, dispatch: dispatch)
default:
return
}
}
}
}
}
func onCapabilitiesChanged(event: CapabilitiesChangedEvent,
state: AppState,
dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard state.callingState.status != .disconnected else {
return
}
do {
let capabilities = try await self.callingService.getCapabilities()
dispatch(.localUserAction(.setCapabilities(capabilities: capabilities)))
let anyLostCapability = event.changedCapabilities.contains(where: { capability in
(capability.type == .unmuteMicrophone && !capability.allowed) ||
(capability.type == .turnVideoOn && !capability.allowed) ||
(capability.type == .manageLobby && !capability.allowed) ||
(capability.type == .removeParticipant && !capability.allowed)
})
if anyLostCapability || !state.localUserState.currentCapabilitiesAreDefault {
var notificationType = ToastNotificationKind.someFeaturesGained
if anyLostCapability {
notificationType = ToastNotificationKind.someFeaturesLost
}
dispatch(.toastNotificationAction(.showNotification(kind: notificationType)))
}
} catch {
self.logger.error("Fetch capabilities Failed with error : \(error)")
}
}
}
func onNetworkQualityCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: NetworkQualityDiagnosticModel) -> Task<Void, Never> {
Task {
if diagnisticModel.value == .bad || diagnisticModel.value == .poor {
switch diagnisticModel.diagnostic {
case .networkReceiveQuality:
dispatch(.toastNotificationAction(.showNotification(kind: .networkReceiveQuality)))
case .networkReconnectionQuality:
dispatch(.toastNotificationAction(.showNotification(kind: .networkReconnectionQuality)))
case .networkSendQuality:
dispatch(.toastNotificationAction(.showNotification(kind: .networkSendQuality)))
}
} else {
dispatch(.toastNotificationAction(.dismissNotification))
}
}
}
func onNetworkCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: NetworkDiagnosticModel) -> Task<Void, Never> {
Task {
if diagnisticModel.value {
switch diagnisticModel.diagnostic {
case .networkRelaysUnreachable:
dispatch(.toastNotificationAction(.showNotification(kind: .networkRelaysUnreachable)))
case .networkUnavailable:
dispatch(.toastNotificationAction(.showNotification(kind: .networkUnavailable)))
}
}
}
}
func onMediaCallDiagnosticsUpdated(state: AppState,
dispatch: @escaping ActionDispatch,
diagnisticModel: MediaDiagnosticModel) -> Task<Void, Never> {
Task {
switch diagnisticModel.diagnostic {
case .speakingWhileMicrophoneIsMuted:
if diagnisticModel.value {
dispatch(.toastNotificationAction(.showNotification(kind: .speakingWhileMicrophoneIsMuted)))
} else {
dispatch(.toastNotificationAction(.dismissNotification))
}
case .cameraStartFailed:
if diagnisticModel.value {
dispatch(.toastNotificationAction(.showNotification(kind: .cameraStartFailed)))
}
case .cameraStartTimedOut:
if diagnisticModel.value {
dispatch(.toastNotificationAction(.showNotification(kind: .cameraStartTimedOut)))
}
default:
break
}
}
}
func dismissNotification(state: AppState, dispatch: @escaping ActionDispatch) -> Task<Void, Never> {
Task {
guard let toastState = state.toastNotificationState.status else {
return
}
switch toastState {
case ToastNotificationKind.networkUnavailable:
dispatch(.callDiagnosticAction(.dismissNetwork(diagnostic: .networkUnavailable)))
case .networkRelaysUnreachable:
dispatch(.callDiagnosticAction(.dismissNetwork(diagnostic: .networkRelaysUnreachable)))
case .networkReceiveQuality:
dispatch(.callDiagnosticAction(.dismissNetworkQuality(diagnostic: .networkReceiveQuality)))
case .networkReconnectionQuality:
dispatch(.callDiagnosticAction(.dismissNetworkQuality(diagnostic: .networkReconnectionQuality)))
case .networkSendQuality:
dispatch(.callDiagnosticAction(.dismissNetworkQuality(diagnostic: .networkSendQuality)))
case .speakingWhileMicrophoneIsMuted:
dispatch(.callDiagnosticAction(.dismissMedia(diagnostic: .speakingWhileMicrophoneIsMuted)))
case .cameraStartFailed:
dispatch(.callDiagnosticAction(.dismissMedia(diagnostic: .cameraStartFailed)))
case .cameraStartTimedOut:
dispatch(.callDiagnosticAction(.dismissMedia(diagnostic: .cameraStartTimedOut)))
case .someFeaturesLost, .someFeaturesGained:
break
}
}
}
func removeParticipant(state: AppState,
dispatch: @escaping ActionDispatch,
participantId: String) -> Task<Void, Never> {
Task {
guard state.callingState.status == .connected else {
return
}
do {
try await callingService.removeParticipant(participantId)
} catch {
dispatch(.remoteParticipantsAction(.removeParticipantError))
}
}
}
}
extension CallingMiddlewareHandler {
private func subscription(dispatch: @escaping ActionDispatch,
isSkipRequested: Bool = false) {
logger.debug("Subscribe to calling service subjects")
callingService.participantsInfoListSubject
.throttle(for: 1.25, scheduler: DispatchQueue.main, latest: true)
.sink { list in
dispatch(.remoteParticipantsAction(.participantListUpdated(participants: list)))
}.store(in: subscription)
callingService.callInfoSubject
.sink { [weak self] callInfoModel in
guard let self = self else {
return
}
let internalError = callInfoModel.internalError
let callingStatus = callInfoModel.status
self.handle(callInfoModel: callInfoModel, dispatch: dispatch, callType: self.callType)
self.logger.debug("Dispatch State Update: \(callingStatus)")
if let internalError = internalError {
self.handleCallInfo(internalError: internalError,
dispatch: dispatch) {
self.logger.debug("Subscription cancelled with Error Code: \(internalError)")
if isSkipRequested {
dispatch(.compositeExitAction)
}
self.subscription.cancel()
}
// to fix the bug that resume call won't work without Internet
// we exit the UI library when we receive the wrong status .remoteHold
} else if callingStatus == .disconnected {
self.logger.debug("Subscription cancel happy path")
dispatch(.compositeExitAction)
self.subscription.cancel()
}
}.store(in: subscription)
callingService.isRecordingActiveSubject
.removeDuplicates()
.sink { isRecordingActive in
dispatch(.callingAction(.recordingStateUpdated(isRecordingActive: isRecordingActive)))
}.store(in: subscription)
callingService.isTranscriptionActiveSubject
.removeDuplicates()
.sink { isTranscriptionActive in
dispatch(.callingAction(.transcriptionStateUpdated(isTranscriptionActive: isTranscriptionActive)))
}.store(in: subscription)
callingService.isLocalUserMutedSubject
.removeDuplicates()
.sink { isLocalUserMuted in
dispatch(.localUserAction(.microphoneMuteStateUpdated(isMuted: isLocalUserMuted)))
}.store(in: subscription)
callingService.callIdSubject
.removeDuplicates()
.sink { callId in
dispatch(.callingAction(.callIdUpdated(callId: callId)))
}.store(in: subscription)
callingService.dominantSpeakersSubject
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
.sink { speakers in
dispatch(.remoteParticipantsAction(.dominantSpeakersUpdated(speakers: speakers)))
}.store(in: subscription)
callingService.participantRoleSubject
.removeDuplicates()
.sink { participantRole in
dispatch(.localUserAction(.participantRoleChanged(participantRole: participantRole)))
}.store(in: subscription)
callingService.totalParticipantCountSubject
.removeDuplicates()
.sink { participantCount in
dispatch(.remoteParticipantsAction(.setTotalParticipantCount(participantCount: participantCount)))
}.store(in: subscription)
/* <CALL_START_TIME>
callingService.callStartTimeSubject
.removeDuplicates()
.sink { startTime in
dispatch(.callingAction(.callStartTimeUpdated(startTime: startTime)))
}.store(in: subscription)
</CALL_START_TIME> */
subscribeOnDiagnostics(dispatch: dispatch)
subscribeCapabilitiesUpdate(dispatch: dispatch)
}
private func subscribeOnDiagnostics(dispatch: @escaping ActionDispatch) {
callingService.networkDiagnosticsSubject
.removeDuplicates()
.sink { networkDiagnostic in
dispatch(.callDiagnosticAction(.network(diagnostic: networkDiagnostic)))
}.store(in: subscription)
callingService.networkQualityDiagnosticsSubject
.removeDuplicates()
.sink { networkQualityDiagnostic in
dispatch(.callDiagnosticAction(.networkQuality(diagnostic: networkQualityDiagnostic)))
}.store(in: subscription)
callingService.mediaDiagnosticsSubject
.removeDuplicates()
.sink { mediaDiagnostic in
dispatch(.callDiagnosticAction(.media(diagnostic: mediaDiagnostic)))
}.store(in: subscription)
callingService.supportedSpokenLanguagesSubject
.removeDuplicates()
.sink { supportSpokenLanguage in
dispatch(.captionsAction(.supportedSpokenLanguagesChanged(languages: supportSpokenLanguage)))
}.store(in: subscription)
callingService.supportedCaptionLanguagesSubject
.sink { supportCaptionsLanguage in
dispatch(.captionsAction(.supportedCaptionLanguagesChanged(languages: supportCaptionsLanguage)))
}.store(in: subscription)
callingService.activeSpokenLanguageSubject
.sink { spokenLanguage in
dispatch(.captionsAction(.spokenLanguageChanged(language: spokenLanguage)))
}.store(in: subscription)
callingService.activeCaptionLanguageSubject
.sink { captionsLanguage in
dispatch(.captionsAction(.captionLanguageChanged(language: captionsLanguage)))
}.store(in: subscription)
callingService.captionsTypeSubject.sink { captionsType in
dispatch(.captionsAction(.typeChanged(type: captionsType)))
}.store(in: subscription)
}
private func subscribeCapabilitiesUpdate(dispatch: @escaping ActionDispatch) {
callingService.capabilitiesChangedSubject
.removeDuplicates()
.sink { event in
dispatch(.localUserAction(.onCapabilitiesChanged(event: event)))
}.store(in: subscription)
}
}
// swiftlint:enable type_body_length