diff --git a/Classes/Swift/Extensions/IOS/OptionalExtensions.swift b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift index ddb0ad754..345b367d7 100644 --- a/Classes/Swift/Extensions/IOS/OptionalExtensions.swift +++ b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift @@ -19,7 +19,7 @@ extension Optional { - var logable: Any { + var orNil: Any { switch self { case .none: return "|⭕️" diff --git a/Classes/Swift/Voip/Models/CallData.swift b/Classes/Swift/Voip/Models/CallData.swift index fb69db380..7d97fdec4 100644 --- a/Classes/Swift/Voip/Models/CallData.swift +++ b/Classes/Swift/Voip/Models/CallData.swift @@ -119,7 +119,7 @@ class CallData { let localAddress = try?Factory.Instance.createAddress(addr: localSipUri), let remoteSipAddress = try?Factory.Instance.createAddress(addr: remoteSipUri) else { - Log.e("[Call] Failed to get either local \(localSipUri.logable) or remote \(remoteSipUri.logable) SIP address!") + Log.e("[Call] Failed to get either local \(localSipUri.orNil) or remote \(remoteSipUri.orNil) SIP address!") return } do { diff --git a/Classes/Swift/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/Models/ConferenceViewModel.swift index 40f9df8bf..4a1aeb2c1 100644 --- a/Classes/Swift/Voip/Models/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/Models/ConferenceViewModel.swift @@ -32,69 +32,104 @@ class ConferenceViewModel { let isConferenceLocallyPaused = MutableLiveData() let isVideoConference = MutableLiveData() let isMeAdmin = MutableLiveData() - + let conference = MutableLiveData() + let conferenceCreationPending = MutableLiveData() let conferenceParticipants = MutableLiveData<[ConferenceParticipantData]>() let conferenceParticipantDevices = MutableLiveData<[ConferenceParticipantDeviceData]>() let conferenceDisplayMode = MutableLiveData() + let isRecording = MutableLiveData() let isRemotelyRecorded = MutableLiveData() + + let participantAdminStatusChangedEvent = MutableLiveData() + let maxParticipantsForMosaicLayout = ConfigManager.instance().lpConfigIntForKey(key: "max_conf_part_mosaic_layout",defaultValue: 6) + let speakingParticipant = MutableLiveData() private var conferenceDelegate : ConferenceDelegateStub? private var coreDelegate : CoreDelegateStub? init () { - conferenceDelegate = ConferenceDelegateStub(onParticipantAdded: { (conference: Conference, participant: Participant) in - Log.i("[Conference] \(conference) Participant \(participant) added") - self.updateParticipantsList(conference) - let count = self.conferenceParticipantDevices.value!.count - if (count > self.maxParticipantsForMosaicLayout) { - Log.w("[Conference] \(conference) More than \(self.maxParticipantsForMosaicLayout) participants \(count), forcing active speaker layout") - self.conferenceDisplayMode.value = .ActiveSpeaker + conferenceDelegate = ConferenceDelegateStub( + onParticipantAdded: { (conference: Conference, participant: Participant) in + Log.i("[Conference] \(conference) Participant \(participant) added") + self.updateParticipantsList(conference) + let count = self.conferenceParticipantDevices.value!.count + if (count > self.maxParticipantsForMosaicLayout) { + Log.w("[Conference] \(conference) More than \(self.maxParticipantsForMosaicLayout) participants \(count), forcing active speaker layout") + self.conferenceDisplayMode.value = .ActiveSpeaker + } + }, + onParticipantRemoved: {(conference: Conference, participant: Participant) in + Log.i("[Conference] \(conference) \(participant) Participant removed") + self.updateParticipantsList(conference) + }, + onParticipantDeviceAdded: {(conference: Conference, participantDevice: ParticipantDevice) in + Log.i("[Conference] \(conference) Participant device \(participantDevice) added") + self.addParticipantDevice(device: participantDevice) + + }, + onParticipantDeviceRemoved: { (conference: Conference, participantDevice: ParticipantDevice) in + Log.i("[Conference] \(conference) Participant device \(participantDevice) removed") + self.removeParticipantDevice(device: participantDevice) + }, + onParticipantAdminStatusChanged: { (conference: Conference, participant: Participant) in + Log.i("[Conference] \(conference) Participant admin status changed") + self.isMeAdmin.value = conference.me?.isAdmin + self.updateParticipantsList(conference) + if let participantData = self.conferenceParticipants.value?.filter ({$0.participant.address!.weakEqual(address2: participant.address!)}).first { + self.participantAdminStatusChangedEvent.value = participantData + } else { + Log.w("[Conference] Failed to find participant [\(participant.address!.asStringUriOnly())] in conferenceParticipants list") + } + }, + onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = true + }, + onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = false + }, + onStateChanged: { (conference: Conference, state: Conference.State) in + Log.i("[Conference] State changed: \(state)") + self.isVideoConference.value = conference.currentParams?.isVideoEnabled + if (state == .Created) { + self.configureConference(conference) + self.conferenceCreationPending.value = false + } + if (state == .TerminationPending) { + self.terminateConference(conference) + } + }, + onSubjectChanged: { (conference: Conference, subject: String) in + self.subject.value = subject + }, + onParticipantDeviceIsSpeakingChanged: { (conference: Conference, participantDevice: ParticipantDevice, isSpeaking:Bool) in + Log.i("[Conference] Participant [\(participantDevice.address!.asStringUriOnly())] is speaking = \(isSpeaking)") + if (isSpeaking) { + if let device = self.conferenceParticipantDevices.value?.filter ({ + $0.participantDevice.address!.weakEqual(address2: participantDevice.address!) + }).first { + self.speakingParticipant.value = device + } else { + Log.w("[Conference] Participant device [\((participantDevice.address?.asStringUriOnly()).orNil)] is speaking but couldn't find it in devices list") + } + + } } - }, onParticipantRemoved: {(conference: Conference, participant: Participant) in - Log.i("[Conference] \(conference) \(participant) Participant removed") - self.updateParticipantsList(conference) - }, onParticipantDeviceAdded: {(conference: Conference, participantDevice: ParticipantDevice) in - Log.i("[Conference] \(conference) Participant device \(participantDevice) added") - self.updateParticipantsDevicesList(conference) - }, onParticipantDeviceRemoved: { (conference: Conference, participantDevice: ParticipantDevice) in - Log.i("[Conference] \(conference) Participant device \(participantDevice) removed") - self.updateParticipantsDevicesList(conference) - }, onParticipantAdminStatusChanged: { (conference: Conference, participant: Participant) in - Log.i("[Conference] \(conference) Participant admin status changed") - self.isMeAdmin.value = conference.me?.isAdmin - self.updateParticipantsList(conference) - }, onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = true - }, onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = false - }, onSubjectChanged: { (conference: Conference, subject: String) in - self.subject.value = subject - } ) coreDelegate = CoreDelegateStub( onConferenceStateChanged: { (core, conference, state) in Log.i("[Conference] \(conference) Conference state changed: \(state)") - self.isVideoConference.value = conference.currentParams?.videoEnabled == true - if (state == Conference.State.Instantiated) { + self.conferenceCreationPending.value = true self.initConference(conference) - } else if (state == Conference.State.Created) { - self.initConference(conference) - self.configureConference(conference) - } else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) { - self.terminateConference(conference) + } - - let layout = conference.layout == .Legacy ? .Grid : conference.layout - self.conferenceDisplayMode.value = layout - Log.i("[Conference] \(conference) Conference current layout is: \(layout)") } ) @@ -105,28 +140,47 @@ class ConferenceViewModel { subject.value = VoipTexts.conference_default_title if let conference = core.conference != nil ? core.conference : core.currentCall?.conference { - Log.i("[Conference] Found an existing conference: \(conference)") - initConference(conference) - configureConference(conference) + Log.i("[Conference] Found an existing conference: \(conference) in state \(conference.state)") + + if (conference.state != .TerminationPending && conference.state != .Terminated) { + initConference(conference) + if (conference.state == Conference.State.Created) { + configureConference(conference) + } else { + conferenceCreationPending.value = true + } + } } - - + } + + func pauseConference() { + Log.i("[Conference] Leaving conference with address \(conference) temporarily") + let _ = conference.value?.leave() + } + + func resumeConference() { + Log.i("[Conference] entering conference with address \(conference)") + let _ = conference.value?.enter() + } + + func toggleRecording() { + if (conference.value?.isRecording == true) { + Log.i("[Conference] Stopping conference recording") + let _ = conference.value?.stopRecording() + } else { + let writablePath = AppManager.recordingFilePathFromCall(address: (conference.value?.conferenceAddress!.asString())!) + Log.i("[Conference] Starting recording in file $path") + let _ = conference.value?.startRecording(path: writablePath) + } + isRecording.value = conference.value?.isRecording } func initConference(_ conference: Conference) { - conferenceExists.value = true + conferenceExists.value = true self.conference.value = conference conference.addDelegate(delegate: self.conferenceDelegate!) - isRecording.value = conference.isRecording - } - - func terminateConference(_ conference: Conference) { - conferenceExists.value = false - isVideoConference.value = false - self.conferenceParticipants.value?.forEach{ $0.destroy()} - self.conferenceParticipantDevices.value?.forEach{ $0.destroy()} - conferenceParticipants.value = [] - conferenceParticipantDevices.value = [] + isRecording.value = conference.isRecording + updateConferenceLayout(conference: conference) } func configureConference(_ conference: Conference) { @@ -136,7 +190,7 @@ class ConferenceViewModel { isConferenceLocallyPaused.value = !conference.isIn self.isMeAdmin.value = conference.me?.isAdmin == true isVideoConference.value = conference.currentParams?.videoEnabled == true - + self.subject.value = conference.subject.isEmpty ? ( conference.me?.isFocus == true ? ( VoipTexts.conference_local_title @@ -149,44 +203,55 @@ class ConferenceViewModel { } - - func pauseConference() { - Log.i("[Conference] Leaving conference with address \(conference) temporarily") - conference.value?.leave() - } - - func resumeConference() { - Log.i("[Conference] entering conference with address \(conference)") - conference.value?.enter() - } - - func togglePlayPause () { - if (isConferenceLocallyPaused.value == true) { - resumeConference() - isConferenceLocallyPaused.value = false - } else { - pauseConference() - isConferenceLocallyPaused.value = true - } - } - - func toggleRecording() { - guard let conference = conference.value else { - Log.e("[Conference] Failed to find conference!") + func addCallsToConference() { + Log.i("[Conference] Trying to merge all calls into existing conference") + guard let conf = conference.value else { return } - /* frogtrust has is own recording method - if (conference.isRecording == true) { - conference.stopRecording() - } else { - let path = AppManager.recordingFilePathFromCall(address: conference.conferenceAddress?.asStringUriOnly() ?? "") - Log.i("[Conference] Starting recording \(conference) in file \(path)") - conference.startRecording(path: path) - }*/ - - isRecording.value = conference.isRecording + core.calls.forEach { call in + if (call.conference == nil) { + try? conf.addParticipant(call: call) + } + } + if (conf.isIn) { + Log.i("[Conference] Conference was paused, resuming it") + let _ = conf.enter() + } } + + func changeLayout(layout: ConferenceLayout) { + Log.i("[Conference] Trying to change conference layout to $layout") + if let conference = conference.value { + conference.layout = layout + updateConferenceLayout(conference: conference) + } else { + Log.e("[Conference] Conference is null in ConferenceViewModel") + } + } + + private func updateConferenceLayout(conference: Conference) { + conferenceDisplayMode.value = conference.layout == .Legacy ? .Grid : conference.layout + let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!) + conferenceParticipantDevices.value = list + Log.i("[Conference] Conference current layout is: \(conference.layout)") + } + + + + func terminateConference(_ conference: Conference) { + conferenceExists.value = false + isVideoConference.value = false + + conference.removeDelegate(delegate: conferenceDelegate!) + + self.conferenceParticipants.value?.forEach{ $0.destroy()} + self.conferenceParticipantDevices.value?.forEach{ $0.destroy()} + conferenceParticipants.value = [] + conferenceParticipantDevices.value = [] + } + + private func updateParticipantsList(_ conference: Conference) { self.conferenceParticipants.value?.forEach{ $0.destroy()} var participants :[ConferenceParticipantData] = [] @@ -232,6 +297,68 @@ class ConferenceViewModel { conferenceParticipantDevices.value = devices } + private func addParticipantDevice(device: ParticipantDevice) { + var devices :[ConferenceParticipantDeviceData] = [] + conferenceParticipantDevices.value?.forEach{devices.append($0)} + + if let deviceAddress = device.address, let _ = devices.filter({ $0.participantDevice.address!.weakEqual(address2: deviceAddress)}).first { + Log.e("[Conference] Participant is already in devices list: \(device.name) (\((device.address?.asStringUriOnly()) ?? "nil")") + return + } + + Log.i("[Conference] New participant device found: \(device.name) (\((device.address?.asStringUriOnly()).orNil)") + let deviceData = ConferenceParticipantDeviceData(participantDevice: device, isMe: false) + devices.append(deviceData) + + let sortedDevices = sortDevicesDataList(devices: devices) + + if (speakingParticipant.value == nil) { + speakingParticipant.value = deviceData + } + + conferenceParticipantDevices.value = sortedDevices + } + + private func removeParticipantDevice(device: ParticipantDevice) { + let devices = conferenceParticipantDevices.value?.filter { + $0.participantDevice.address?.asStringUriOnly() != device.address?.asStringUriOnly() + } + if (devices?.count == conferenceParticipantDevices.value?.count) { + Log.e("[Conference] Failed to remove participant device: \(device.name) (\((device.address?.asStringUriOnly()).orNil)") + } else { + Log.i("[Conference] Participant device removed: \(device.name) (\((device.address?.asStringUriOnly()).orNil)") + } + + conferenceParticipantDevices.value = devices + } + + + private func sortDevicesDataList(devices: [ConferenceParticipantDeviceData]) -> [ConferenceParticipantDeviceData] { + if let meDeviceData = devices.filter({$0.isMe}).first { + var devicesWithoutMe = devices.filter { !$0.isMe } + if (conferenceDisplayMode.value == .ActiveSpeaker) { + devicesWithoutMe.insert(meDeviceData, at: 0) + } else { + devicesWithoutMe.append(meDeviceData) + } + return devicesWithoutMe + } + return devices + } + + + func togglePlayPause () { + if (isConferenceLocallyPaused.value == true) { + resumeConference() + isConferenceLocallyPaused.value = false + } else { + pauseConference() + isConferenceLocallyPaused.value = true + } + } + + // Review below (dynamic add/remove) + func updateParticipants(addresses:[Address]) { guard let conference = conference.value else { Log.w("[Conference Participants] conference not set, can't update participants") @@ -252,7 +379,7 @@ class ConferenceViewModel { try conference.participantList.forEach { participant in let member = addresses.filter { $0.asStringUriOnly() == participant.address?.asStringUriOnly() }.first if (member == nil) { - Log.w("[Conference Participants] Participant \(participant.address?.asStringUriOnly()) will be removed from conference") + Log.w("[Conference Participants] Participant \((participant.address?.asStringUriOnly()).orNil) will be removed from conference") try conference.removeParticipant(participant: participant) } } @@ -261,19 +388,6 @@ class ConferenceViewModel { } } - func addCallsToConference() { - Log.i("[Conference] Trying to merge all calls into existing conference") - guard let conf = conference.value else { - return - } - core.calls.forEach { call in - if (call.conference == nil) { - try? conf.addParticipant(call: call) - } - } - } - - } @objc class ConferenceViewModelBridge : NSObject { diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 630b1cb01..adb32ae1f 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -311,7 +311,6 @@ import linphonesw } func goToChat() { - let core = Core.get() guard let chatRoom = CallsViewModel.shared.currentCallData.value??.chatRoom else { diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index c99d5f240..e9dd7883f 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -182,7 +182,6 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi if (width > 0) { self.grid.removeConstraints().width(width).height(height).center().done() } - Log.e("cdes > \(width)") } } @@ -244,7 +243,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { + guard let _ = conferenceViewModel?.conferenceParticipantDevices.value?.count else { return .zero }