feat: implement Linphone SDK integration with video call support
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
This commit is contained in:
parent
7bbaf1b827
commit
0375fe4d1a
18 changed files with 901 additions and 52 deletions
|
|
@ -2,18 +2,261 @@ import Flutter
|
|||
import UIKit
|
||||
|
||||
public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "liblinphone_flutter", binaryMessenger: registrar.messenger())
|
||||
let instance = LiblinphoneFlutterPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "getPlatformVersion":
|
||||
result("iOS " + UIDevice.current.systemVersion)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue