Commit 8ee3e46d authored by Christophe Deschamps's avatar Christophe Deschamps
Browse files

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

Showing with 218 additions and 74 deletions
......@@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 */,
);
......
......@@ -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)
}
}
......
......@@ -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()
......
//
// 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
}
// ...
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment