Complete rewrite from stub plugin to (probably) functional liblinphone integration featuring: - Core SDK bridge with registration, calls, and media controls - Platform views for remote and local video rendering - Event channels for registration and call state updates
262 lines
9.1 KiB
Swift
262 lines
9.1 KiB
Swift
import Flutter
|
|
import UIKit
|
|
|
|
public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
|
|
private var channel: FlutterMethodChannel!
|
|
private var registrationEventsChannel: FlutterEventSink?
|
|
private var callEventsChannel: FlutterEventSink?
|
|
|
|
private var activity: UIViewController?
|
|
private var remoteViewCache: [Int: UIView] = [:]
|
|
private var localViewCache: [Int: UIView] = [:]
|
|
|
|
private var linphoneBridge: LinphoneBridge!
|
|
|
|
private static let TAG = "LiblinphoneFlutterPlugin"
|
|
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
let channel = FlutterMethodChannel(
|
|
name: "liblinphone_flutter",
|
|
binaryMessenger: registrar.messenger()
|
|
)
|
|
let instance = LiblinphoneFlutterPlugin()
|
|
instance.channel = channel
|
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
|
|
// Register platform views
|
|
let remoteViewFactory = RemoteViewFactory(
|
|
messenger: registrar.messenger(),
|
|
cacher: { view in
|
|
print("[\(TAG)] Caching RemoteView")
|
|
instance.remoteViewCache[0] = view
|
|
}
|
|
)
|
|
registrar.register(
|
|
remoteViewFactory,
|
|
withId: "liblinphone_flutter.nuark.xyz/remote_view"
|
|
)
|
|
|
|
let localViewFactory = LocalViewFactory(
|
|
messenger: registrar.messenger(),
|
|
cacher: { view in
|
|
print("[\(TAG)] Caching LocalView")
|
|
instance.localViewCache[0] = view
|
|
}
|
|
)
|
|
registrar.register(
|
|
localViewFactory,
|
|
withId: "liblinphone_flutter.nuark.xyz/local_view"
|
|
)
|
|
|
|
// Setup event channels
|
|
let registrationEventChannel = FlutterEventChannel(
|
|
name: "liblinphone_flutter.nuark.xyz/registration_events",
|
|
binaryMessenger: registrar.messenger()
|
|
)
|
|
let registrationStreamHandler = EventStreamHandler(
|
|
onListen: { sink in
|
|
instance.registrationEventsChannel = sink
|
|
},
|
|
onCancel: {
|
|
instance.registrationEventsChannel = nil
|
|
}
|
|
)
|
|
registrationEventChannel.setStreamHandler(registrationStreamHandler)
|
|
|
|
let callEventChannel = FlutterEventChannel(
|
|
name: "liblinphone_flutter.nuark.xyz/call_events",
|
|
binaryMessenger: registrar.messenger()
|
|
)
|
|
let callStreamHandler = EventStreamHandler(
|
|
onListen: { sink in
|
|
instance.callEventsChannel = sink
|
|
},
|
|
onCancel: {
|
|
instance.callEventsChannel = nil
|
|
}
|
|
)
|
|
callEventChannel.setStreamHandler(callStreamHandler)
|
|
|
|
// Get root view controller
|
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
let window = windowScene.windows.first,
|
|
let rootViewController = window.rootViewController {
|
|
instance.activity = rootViewController
|
|
}
|
|
}
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "checkPermissions":
|
|
let hasPermissions = linphoneBridge.checkPermissions()
|
|
result(hasPermissions)
|
|
|
|
case "initialize":
|
|
do {
|
|
guard let activity = self.activity else {
|
|
result(FlutterError(
|
|
code: "NO_ACTIVITY",
|
|
message: "Activity not available",
|
|
details: nil
|
|
))
|
|
return
|
|
}
|
|
|
|
linphoneBridge = LinphoneBridge(
|
|
activity: activity,
|
|
remoteViewAcquisitor: { [weak self] in
|
|
return self?.acquireRemoteView()
|
|
},
|
|
localViewAcquisitor: { [weak self] in
|
|
return self?.acquireLocalView()
|
|
},
|
|
onRegistrationStateChanged: { [weak self] state in
|
|
self?.registrationEventsChannel?(state)
|
|
},
|
|
onCallStateChanged: { [weak self] state in
|
|
self?.callEventsChannel?(state)
|
|
}
|
|
)
|
|
linphoneBridge.initializeLinphone()
|
|
result(true)
|
|
} catch {
|
|
print("[\(LiblinphoneFlutterPlugin.TAG)] initialize error: \(error.localizedDescription)")
|
|
result(FlutterError(
|
|
code: "ERROR",
|
|
message: error.localizedDescription,
|
|
details: nil
|
|
))
|
|
}
|
|
|
|
case "register":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let username = args["username"] as? String,
|
|
let password = args["password"] as? String,
|
|
let serverIp = args["serverIp"] as? String,
|
|
let serverPort = args["serverPort"] as? Int else {
|
|
result(FlutterError(
|
|
code: "INVALID_ARGUMENTS",
|
|
message: "Missing required arguments",
|
|
details: nil
|
|
))
|
|
return
|
|
}
|
|
|
|
do {
|
|
linphoneBridge.register(
|
|
username: username,
|
|
password: password,
|
|
serverIp: serverIp,
|
|
serverPort: serverPort
|
|
)
|
|
result(true)
|
|
} catch {
|
|
print("[\(LiblinphoneFlutterPlugin.TAG)] register error: \(error.localizedDescription)")
|
|
result(FlutterError(
|
|
code: "ERROR",
|
|
message: error.localizedDescription,
|
|
details: nil
|
|
))
|
|
}
|
|
|
|
case "unregister":
|
|
linphoneBridge.unregister()
|
|
result(true)
|
|
|
|
case "makeCall":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let callTo = args["callTo"] as? String,
|
|
let isVideoEnabled = args["isVideoEnabled"] as? Bool else {
|
|
result(FlutterError(
|
|
code: "INVALID_ARGUMENTS",
|
|
message: "Missing required arguments",
|
|
details: nil
|
|
))
|
|
return
|
|
}
|
|
|
|
do {
|
|
try linphoneBridge.makeCall(callTo: callTo, isVideoEnabled: isVideoEnabled)
|
|
result(true)
|
|
} catch {
|
|
print("[\(LiblinphoneFlutterPlugin.TAG)] makeCall error: \(error.localizedDescription)")
|
|
result(FlutterError(
|
|
code: "ERROR",
|
|
message: error.localizedDescription,
|
|
details: nil
|
|
))
|
|
}
|
|
|
|
case "answerCall":
|
|
let success = linphoneBridge.answerCall()
|
|
result(success)
|
|
|
|
case "hangupCall":
|
|
let success = linphoneBridge.hangupCall()
|
|
result(success)
|
|
|
|
case "inCall":
|
|
let inCall = linphoneBridge.inCall()
|
|
result(inCall)
|
|
|
|
case "callType":
|
|
let callType = linphoneBridge.callType()
|
|
let ordinal: Int
|
|
switch callType {
|
|
case .audio:
|
|
ordinal = 0
|
|
case .video:
|
|
ordinal = 1
|
|
case .unknown:
|
|
ordinal = 2
|
|
}
|
|
result(ordinal)
|
|
|
|
case "toggleVideo":
|
|
let enabled = linphoneBridge.toggleVideo()
|
|
result(enabled)
|
|
|
|
case "toggleMicrophone":
|
|
let enabled = linphoneBridge.toggleMicrophone()
|
|
result(enabled)
|
|
|
|
case "stop":
|
|
linphoneBridge.stop()
|
|
result(true)
|
|
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
internal func acquireLocalView() -> UIView? {
|
|
print("[\(LiblinphoneFlutterPlugin.TAG)] acquireLocalView: \(localViewCache.count)")
|
|
return localViewCache[0]
|
|
}
|
|
|
|
internal func acquireRemoteView() -> UIView? {
|
|
print("[\(LiblinphoneFlutterPlugin.TAG)] acquireRemoteView: \(remoteViewCache.count)")
|
|
return remoteViewCache[0]
|
|
}
|
|
}
|
|
|
|
// MARK: - Event Stream Handler
|
|
class EventStreamHandler: NSObject, FlutterStreamHandler {
|
|
private let onListenCallback: (@escaping FlutterEventSink) -> Void
|
|
private let onCancelCallback: () -> Void
|
|
|
|
init(onListen: @escaping (@escaping FlutterEventSink) -> Void, onCancel: @escaping () -> Void) {
|
|
self.onListenCallback = onListen
|
|
self.onCancelCallback = onCancel
|
|
}
|
|
|
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
onListenCallback(events)
|
|
return nil
|
|
}
|
|
|
|
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
onCancelCallback()
|
|
return nil
|
|
}
|
|
}
|