From 8ee3e46dbc2a05655f29c437b9477b3f0479a2ed Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 13 Sep 2023 14:04:51 +0200 Subject: [PATCH] Suggestions from CD for Async Call backs and Core objects associated with publishers --- .../CallKitTutorial.xcodeproj/project.pbxproj | 4 + .../CallKitProviderDelegate.swift | 4 +- .../CallKitTutorial/CallKitTutorial.swift | 163 ++++++++++-------- .../CallKitTutorial/LinphoneAsyncHelper.swift | 121 +++++++++++++ 4 files changed, 218 insertions(+), 74 deletions(-) create mode 100644 ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj b/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj index cc25697..f07abb4 100644 --- a/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj +++ b/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 6608A97224E197D5006E6C68 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6608A97024E197D5006E6C68 /* LaunchScreen.storyboard */; }; 6608A97A24E19817006E6C68 /* CallKitTutorial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6608A97924E19817006E6C68 /* CallKitTutorial.swift */; }; 6608A97C24E1981E006E6C68 /* CallKitProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6608A97B24E1981E006E6C68 /* CallKitProviderDelegate.swift */; }; + C6FFD82E2AB199FA00DB168D /* LinphoneAsyncHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +33,7 @@ 6608A97D24E19852006E6C68 /* CallKitTutorial.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CallKitTutorial.entitlements; sourceTree = ""; }; 9D767CD0AA573757AD042365 /* Pods-CallKitTutorial.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitTutorial.release.xcconfig"; path = "Target Support Files/Pods-CallKitTutorial/Pods-CallKitTutorial.release.xcconfig"; sourceTree = ""; }; C6E14A45B7F8F19111223509 /* Pods_CallKitTutorial.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallKitTutorial.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneAsyncHelper.swift; sourceTree = ""; }; D7FA91A3C73CAEE3300CB14C /* Pods-CallKitTutorial.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitTutorial.debug.xcconfig"; path = "Target Support Files/Pods-CallKitTutorial/Pods-CallKitTutorial.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -78,6 +80,7 @@ isa = PBXGroup; children = ( 6608A97D24E19852006E6C68 /* CallKitTutorial.entitlements */, + C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */, 6608A96524E197D5006E6C68 /* AppDelegate.swift */, 6608A96724E197D5006E6C68 /* SceneDelegate.swift */, 6608A97B24E1981E006E6C68 /* CallKitProviderDelegate.swift */, @@ -225,6 +228,7 @@ 6608A97A24E19817006E6C68 /* CallKitTutorial.swift in Sources */, 6608A96624E197D5006E6C68 /* AppDelegate.swift in Sources */, 6608A96824E197D5006E6C68 /* SceneDelegate.swift in Sources */, + C6FFD82E2AB199FA00DB168D /* LinphoneAsyncHelper.swift in Sources */, 6608A96A24E197D5006E6C68 /* ContentView.swift in Sources */, 6608A97C24E1981E006E6C68 /* CallKitProviderDelegate.swift in Sources */, ); diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift index fb9101b..97fffc8 100644 --- a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift +++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift @@ -109,14 +109,14 @@ extension CallKitProviderDelegate: CXProviderDelegate { NSLog("Callkit didActivateaudiosession -- Is main thread ? \(Thread.isMainThread ? "yes" : "no") ") // The linphone Core must be notified that CallKit has activated the AVAudioSession // in order to start streaming audio. - tutorialContext.postOnCoreQueue { + tutorialContext.linphoneAsyncHelper.postOnCoreQueue { self.tutorialContext.mCore.activateAudioSession(actived: true) } } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { // The linphone Core must be notified that CallKit has deactivated the AVAudioSession. - tutorialContext.postOnCoreQueue { + tutorialContext.linphoneAsyncHelper.postOnCoreQueue { self.tutorialContext.mCore.activateAudioSession(actived: false) } } diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift index cd6394b..a0a144c 100644 --- a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift +++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift @@ -8,17 +8,15 @@ import linphonesw import AVFoundation - +import Combine class CallKitExampleContext : ObservableObject { - private let queue = DispatchQueue(label:"core.queue") var mCore: Core! var mAccount: Account? - var mCoreDelegate : CoreDelegate! var mIterateTimer : Timer! - + @Published var coreVersion: String = Core.getVersion @Published var username : String = "quentindev" @Published var passwd : String = "dev" @@ -32,101 +30,122 @@ class CallKitExampleContext : ObservableObject @Published var isSpeakerEnabled : Bool = false @Published var isMicrophoneEnabled : Bool = false + /* Async */ + let linphoneAsyncHelper = LinphoneAsyncHelper() + + /*------------ Callkit tutorial related variables ---------------*/ let incomingCallName = "Incoming call example" var mCall : Call? var mProviderDelegate : CallKitProviderDelegate! var mCallAlreadyStopped : Bool = false; + - func postOnCoreQueue(lambda : @escaping ()->()) { - queue.async { - lambda() - } + func addRegistrationStateCallBack(core:Core) { + core.createAccountRegistrationStateChangedPublisher() + .postOnMainQueue { result in + NSLog("New registration state is \(result.state) for user id \( String(describing: result.account.params?.identityAddress?.asString()))\n") + if (result.state == .Ok) { + self.loggedIn = true + // Since core has "Push Enabled", the reception and setting of the push notification token is done automatically + // It should have been set and used when we log in, you can check here or in the liblinphone logs + NSLog("Account registered Push voip token: \(result.account.params?.pushNotificationConfig?.voipToken)") + } else if (result.state == .Cleared) { + self.loggedIn = false + } + } + .postOnCoreQueue{ result in + // optional something on core queue if needed + } } - func postOnMainQueue(lambda : @escaping()->()) { - DispatchQueue.main.async { - lambda() - } + + func addCallStateChangedCallBack(core:Core) { + core.createOnCallStateChangedPublisher() + .postOnMainQueue { result in + self.callMsg = result.message + if (result.state == .PushIncomingReceived){ + // We're being called by someone (and app is in background) + self.mCall = result.call + self.mProviderDelegate.incomingCall() + self.isCallIncoming = true + self.callMsg = result.message + } else if (result.state == .IncomingReceived) { + // If app is in foreground, it's likely that we will receive the SIP invite before the Push notification + if (!self.isCallIncoming) { + self.mCall = result.call + self.mProviderDelegate.incomingCall() + + self.isCallIncoming = true + self.callMsg = result.message + } + self.remoteAddress = result.call.remoteAddress!.asStringUriOnly() + } else if (result.state == .Connected) { + self.isCallIncoming = false + self.isCallRunning = true + } else if (result.state == .Released || result.state == .End || result.state == .Error) { + // Call has been terminated by any side + + // Report to CallKit that the call is over, if the terminate action was initiated by other end of the call + if (self.isCallRunning) { + self.mProviderDelegate.stopCall() + } + self.remoteAddress = "Nobody yet" + } + } + .postOnCoreQueue{ result in + // optional something on core queue if needed + } } init() { LoggingService.Instance.logLevel = LogLevel.Debug - let factory = Factory.Instance // IMPORTANT : In this tutorial, we require the use of a core configuration file. // This way, once the registration is done, and until it is cleared, it will return to the LoggedIn state on launch. // This allows us to have a functional call when the app was closed and is started by a VOIP push notification (incoming call // We also need to enable "Push Notitifications" and "Background Mode - Voice Over IP" - let configDir = factory.getConfigDir(context: nil) - try? mCore = factory.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) - // enabling push notifications management in the core - mCore.callkitEnabled = true - mCore.pushNotificationEnabled = true - mCore.autoIterateEnabled = false - - mCoreDelegate = CoreDelegateStub( onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in - self.callMsg = message - if (state == .PushIncomingReceived){ - // We're being called by someone (and app is in background) - self.mCall = call - self.mProviderDelegate.incomingCall() - self.isCallIncoming = true - self.callMsg = message - } else if (state == .IncomingReceived) { - // If app is in foreground, it's likely that we will receive the SIP invite before the Push notification - if (!self.isCallIncoming) { - self.mCall = call - self.mProviderDelegate.incomingCall() - - self.isCallIncoming = true - self.callMsg = message + linphoneAsyncHelper.postOnCoreQueue { + let factory = Factory.Instance + let configDir = factory.getConfigDir(context: nil) + let corePublisher = self.linphoneAsyncHelper.createLinphoneObjectWithPublisher(createAction: { + try factory.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + }) + corePublisher + .postOnCoreQueue ( + onError: { error in + NSLog("failed creating core \(error)") + }, + receiveValue: { core in + self.mCore = core + // enabling push notifications management in the core + self.mCore.callkitEnabled = true + self.mCore.pushNotificationEnabled = true + self.mCore.autoIterateEnabled = false + self.addRegistrationStateCallBack(core: core) + self.addCallStateChangedCallBack(core: core) + try? core.start() + }) + .postOnMainQueue { core in + self.mIterateTimer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in + self?.linphoneAsyncHelper.postOnCoreQueue { + core.iterate() + } } - self.remoteAddress = call.remoteAddress!.asStringUriOnly() - } else if (state == .Connected) { - self.isCallIncoming = false - self.isCallRunning = true - } else if (state == .Released || state == .End || state == .Error) { - // Call has been terminated by any side - - // Report to CallKit that the call is over, if the terminate action was initiated by other end of the call - if (self.isCallRunning) { - self.mProviderDelegate.stopCall() - } - self.remoteAddress = "Nobody yet" - } - }, onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in - NSLog("New registration state is \(state) for user id \( String(describing: account.params?.identityAddress?.asString()))\n") - if (state == .Ok) { - self.loggedIn = true - // Since core has "Push Enabled", the reception and setting of the push notification token is done automatically - // It should have been set and used when we log in, you can check here or in the liblinphone logs - NSLog("Account registered Push voip token: \(account.params?.pushNotificationConfig?.voipToken)") - } else if (state == .Cleared) { - self.loggedIn = false - } - }) - - mIterateTimer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in - self?.postOnCoreQueue { - self?.mCore.iterate() } + } - + mProviderDelegate = CallKitProviderDelegate(context: self) - mCore.addDelegate(delegate: mCoreDelegate) - postOnCoreQueue { - try? self.mCore.start() - } } func login() { - postOnCoreQueue { + linphoneAsyncHelper.postOnCoreQueue { do { var transport : TransportType if (self.transportType == "TLS") { transport = TransportType.Tls } @@ -156,7 +175,7 @@ class CallKitExampleContext : ObservableObject func unregister() { - postOnCoreQueue { + linphoneAsyncHelper.postOnCoreQueue { if let account = self.mCore.defaultAccount { let params = account.params let clonedParams = params?.clone() @@ -166,7 +185,7 @@ class CallKitExampleContext : ObservableObject } } func delete() { - postOnCoreQueue { + linphoneAsyncHelper.postOnCoreQueue { if let account = self.mCore.defaultAccount { self.mCore.removeAccount(account: account) self.mCore.clearAccounts() diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift new file mode 100644 index 0000000..f957f21 --- /dev/null +++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift @@ -0,0 +1,121 @@ +// +// LinphoneAsyncWrapper.swift +// CallKitTutorial +// +// Created by CD on 13/09/2023. +// Copyright © 2023 BelledonneCommunications. All rights reserved. +// + +import Foundation +import Combine +import linphonesw + +var coreQueue : DispatchQueue = DispatchQueue(label:"core.queue") +var cancellables = Set() + +// A publisher object that can old one or many LinphoneObject objects. + +public class LinphoneObjectsPublisher : Publisher { + public typealias Output = T + public typealias Failure = Error + let passThroughSubject = PassthroughSubject() + public func receive(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output { + passThroughSubject.receive(subscriber: subscriber) + } + + public func send(completion: Subscribers.Completion) { + passThroughSubject.send(completion: completion) + } + + @discardableResult + public func postOnMainQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) -> LinphoneObjectsPublisher { + doOnQueue(onError,receiveValue,queue: DispatchQueue.main) + return self + } + + @discardableResult + public func postOnCoreQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) -> LinphoneObjectsPublisher { + doOnQueue(onError,receiveValue,queue: coreQueue) + return self + } + + + private func doOnQueue(_ onError :@escaping ((Error) -> Void) = {_ in }, _ receiveValue:@escaping ((Output) -> Void), queue:DispatchQueue) { + passThroughSubject.receive(on:queue) + .sink { error in + onError(error as! Error) + } receiveValue: { result in + receiveValue(result) + }.store(in: &cancellables) + } + + var savedReference : Any? = nil // Used when a reference is needed to avoid object from beeing GCd (example delegate stubs) + convenience init (reference: Any) { + self.init() + savedReference = reference + } +} + +public class LinphoneAsyncHelper { + + func postOnCoreQueue(lambda : @escaping ()->()) { + coreQueue.async { + lambda() + } + } + + func postOnMainQueue(lambda : @escaping()->()) { + DispatchQueue.main.async { + lambda() + } + } + + // Creates a publisher from the object created by the action passed as parameter + // For example if passed a create core call this function will create the LinphoneObject Core on core queue, and created object will be published through the built publisher + func createLinphoneObjectWithPublisher(createAction:@escaping()throws -> LinphoneObject ) -> LinphoneObjectsPublisher { + let publisher = LinphoneObjectsPublisher() + coreQueue.async { + do { + publisher.passThroughSubject.send(try createAction()) + } catch { + publisher.send(completion: .failure(error)) + } + } + return publisher + } + +} + +extension Core { + + // Methods below would generated by a script similar to the one that creates LinphoneWrapper + + public func createAccountRegistrationStateChangedPublisher() -> LinphoneObjectsPublisher<(core:Core, account:Account, state:RegistrationState, message:String)> { + let publisher = LinphoneObjectsPublisher<(core:Core, account:Account, state:RegistrationState, message:String)>() + let coreDelegate = CoreDelegateStub ( + onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in + publisher.passThroughSubject.send((core,account,state,message)) + }) + publisher.savedReference = coreDelegate + addDelegate(delegate: coreDelegate) + return publisher + } + + public func createOnCallStateChangedPublisher() -> LinphoneObjectsPublisher<(core: Core, call: Call, state: Call.State, message: String)> { + let publisher = LinphoneObjectsPublisher<(core: Core, call: Call, state: Call.State, message: String)>() + let coreDelegate = CoreDelegateStub ( + onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in + publisher.passThroughSubject.send((core,call,state,message)) + }) + publisher.savedReference = coreDelegate + addDelegate(delegate: coreDelegate) + return publisher + } + + + + // ... +} + + +