Suggestions from CD for Async Call backs and Core objects associated with publishers

This commit is contained in:
Christophe Deschamps
2023-09-13 14:04:51 +02:00
parent 59d7747e86
commit 8ee3e46dbc
4 changed files with 218 additions and 74 deletions

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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<AnyCancellable>()
// A publisher object that can old one or many LinphoneObject objects.
public class LinphoneObjectsPublisher <T> : Publisher {
public typealias Output = T
public typealias Failure = Error
let passThroughSubject = PassthroughSubject<Output, Failure>()
public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
passThroughSubject.receive(subscriber: subscriber)
}
public func send(completion: Subscribers.Completion<Failure>) {
passThroughSubject.send(completion: completion)
}
@discardableResult
public func postOnMainQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) -> LinphoneObjectsPublisher <T>{
doOnQueue(onError,receiveValue,queue: DispatchQueue.main)
return self
}
@discardableResult
public func postOnCoreQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) -> LinphoneObjectsPublisher <T>{
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<LinphoneObject>(createAction:@escaping()throws -> LinphoneObject ) -> LinphoneObjectsPublisher<LinphoneObject> {
let publisher = LinphoneObjectsPublisher<LinphoneObject>()
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
}
// ...
}