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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
312
ios/Classes/LinphoneBridge.swift
Normal file
312
ios/Classes/LinphoneBridge.swift
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import linphonesw
|
||||
import AVFoundation
|
||||
|
||||
class LinphoneBridge {
|
||||
private var core: Core!
|
||||
private var isRegistered = false
|
||||
private var currentCall: Call?
|
||||
private let activity: UIViewController
|
||||
private let remoteViewAcquisitor: () -> UIView?
|
||||
private let localViewAcquisitor: () -> UIView?
|
||||
private let onRegistrationStateChanged: (Int) -> Void
|
||||
private let onCallStateChanged: (Int) -> Void
|
||||
|
||||
private var registrationState: RegistrationState = .None {
|
||||
didSet {
|
||||
print("registrationState delegate: oldValue: \(oldValue), newValue: \(registrationState)")
|
||||
onRegistrationStateChanged(registrationState.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
private var callState: Call.State = .Idle {
|
||||
didSet {
|
||||
print("callState delegate: oldValue: \(oldValue), newValue: \(callState)")
|
||||
onCallStateChanged(callState.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
private static let TAG = "LinphoneBridge"
|
||||
|
||||
init(
|
||||
activity: UIViewController,
|
||||
remoteViewAcquisitor: @escaping () -> UIView?,
|
||||
localViewAcquisitor: @escaping () -> UIView?,
|
||||
onRegistrationStateChanged: @escaping (Int) -> Void,
|
||||
onCallStateChanged: @escaping (Int) -> Void
|
||||
) {
|
||||
self.activity = activity
|
||||
self.remoteViewAcquisitor = remoteViewAcquisitor
|
||||
self.localViewAcquisitor = localViewAcquisitor
|
||||
self.onRegistrationStateChanged = onRegistrationStateChanged
|
||||
self.onCallStateChanged = onCallStateChanged
|
||||
}
|
||||
|
||||
func checkPermissions() -> Bool {
|
||||
let audioStatus = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
|
||||
if audioStatus != .authorized || videoStatus != .authorized {
|
||||
// Request permissions
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in }
|
||||
AVCaptureDevice.requestAccess(for: .video) { _ in }
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func initializeLinphone() {
|
||||
do {
|
||||
let factory = Factory.Instance
|
||||
core = try factory.createCore(configPath: nil, factoryConfigPath: nil, systemContext: nil)
|
||||
|
||||
// Add core listener
|
||||
core.addDelegate(delegate: self)
|
||||
|
||||
// Enable video
|
||||
core.videoCaptureEnabled = true
|
||||
core.videoDisplayEnabled = true
|
||||
core.videoActivationPolicy?.automaticallyInitiate = true
|
||||
core.videoActivationPolicy?.automaticallyAccept = true
|
||||
|
||||
// DSCP settings
|
||||
core.sipDscp = 26 // AF31
|
||||
core.audioDscp = 46 // EF
|
||||
core.videoDscp = 36 // AF42
|
||||
|
||||
core.videoAdaptiveJittcompEnabled = true
|
||||
|
||||
// Bandwidth settings
|
||||
core.uploadBandwidth = 512
|
||||
core.downloadBandwidth = 1500
|
||||
|
||||
// Configure audio codecs
|
||||
let preferredAudio = ["opus", "pcmu", "pcma"]
|
||||
for pt in core.audioPayloadTypes {
|
||||
let mime = pt.mimeType.lowercased()
|
||||
let enabled = preferredAudio.contains(mime)
|
||||
let ok = pt.enable(enabled: enabled) == 0
|
||||
print("\(LinphoneBridge.TAG) Change state of \(mime) to \(enabled) = \(ok)")
|
||||
}
|
||||
|
||||
// Configure video codecs
|
||||
let preferredVideo = ["h264", "vp8"]
|
||||
for pt in core.videoPayloadTypes {
|
||||
let mime = pt.mimeType.lowercased()
|
||||
let enabled = preferredVideo.contains(mime)
|
||||
let ok = pt.enable(enabled: enabled) == 0
|
||||
print("\(LinphoneBridge.TAG) Change state of \(mime) to \(enabled) = \(ok)")
|
||||
}
|
||||
|
||||
// Set bitrates
|
||||
core.getPayloadType(type: "opus", rate: -1, channels: 0)?.normalBitrate = 32
|
||||
core.getPayloadType(type: "h264", rate: -1, channels: 0)?.normalBitrate = 600
|
||||
core.getPayloadType(type: "vp8", rate: -1, channels: 0)?.normalBitrate = 600
|
||||
|
||||
// Video settings
|
||||
let preferredVidDef = try factory.createVideoDefinition(width: 720, height: 1280)
|
||||
core.preferredVideoDefinition = preferredVidDef
|
||||
core.preferredFramerate = 30
|
||||
|
||||
try core.start()
|
||||
} catch {
|
||||
print("Error initializing Linphone: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func register(username: String, password: String, serverIp: String, serverPort: Int) {
|
||||
do {
|
||||
let factory = Factory.Instance
|
||||
let authInfo = try factory.createAuthInfo(
|
||||
username: username,
|
||||
userid: nil,
|
||||
passwd: password,
|
||||
ha1: nil,
|
||||
realm: nil,
|
||||
domain: serverIp
|
||||
)
|
||||
core.addAuthInfo(info: authInfo)
|
||||
|
||||
let identity = try factory.createAddress(addr: "sip:\(username)@\(serverIp)")
|
||||
let server = try factory.createAddress(addr: "sip:\(serverIp):\(serverPort)")
|
||||
|
||||
let accountParams = try core.createAccountParams()
|
||||
try accountParams.setIdentityaddress(newValue: identity)
|
||||
try accountParams.setServeraddress(newValue: server)
|
||||
accountParams.registerEnabled = true
|
||||
|
||||
let account = try core.createAccount(params: accountParams)
|
||||
try core.addAccount(account: account)
|
||||
core.defaultAccount = account
|
||||
} catch {
|
||||
print("Error registering: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func unregister() {
|
||||
core.clearAccounts()
|
||||
core.clearAllAuthInfo()
|
||||
}
|
||||
|
||||
func makeCall(callTo: String, isVideoEnabled: Bool) throws {
|
||||
guard isRegistered else {
|
||||
throw NSError(domain: "LinphoneBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not registered"])
|
||||
}
|
||||
|
||||
do {
|
||||
let factory = Factory.Instance
|
||||
guard let remoteAddress = try? factory.createAddress(addr: callTo) else {
|
||||
throw NSError(domain: "LinphoneBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create remote address"])
|
||||
}
|
||||
|
||||
guard let callParams = try? core.createCallParams(call: nil) else {
|
||||
throw NSError(domain: "LinphoneBridge", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create call params"])
|
||||
}
|
||||
|
||||
callParams.videoEnabled = isVideoEnabled
|
||||
callParams.videoDirection = isVideoEnabled ? .SendRecv : .Inactive
|
||||
|
||||
currentCall = try core.inviteAddressWithParams(addr: remoteAddress, params: callParams)
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func answerCall() -> Bool {
|
||||
guard let call = currentCall else {
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
let callParams = try core.createCallParams(call: call)
|
||||
callParams.videoEnabled = true
|
||||
callParams.videoDirection = .SendRecv
|
||||
try call.acceptWithParams(params: callParams)
|
||||
return true
|
||||
} catch {
|
||||
print("Error answering call: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hangupCall() -> Bool {
|
||||
do {
|
||||
try currentCall?.terminate()
|
||||
return true
|
||||
} catch {
|
||||
print("Error hanging up: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func toggleVideo() -> Bool {
|
||||
guard let call = currentCall else {
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
let params = try core.createCallParams(call: call)
|
||||
params.videoEnabled = !(call.currentParams?.videoEnabled ?? false)
|
||||
try call.update(params: params)
|
||||
return call.currentParams?.videoEnabled ?? false
|
||||
} catch {
|
||||
print("Error toggling video: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func toggleMicrophone() -> Bool {
|
||||
core.micEnabled = !core.micEnabled
|
||||
return core.micEnabled
|
||||
}
|
||||
|
||||
func stop() {
|
||||
core.stop()
|
||||
}
|
||||
|
||||
func inCall() -> Bool {
|
||||
return currentCall != nil
|
||||
}
|
||||
|
||||
enum CallType {
|
||||
case audio
|
||||
case video
|
||||
case unknown
|
||||
}
|
||||
|
||||
func callType() -> CallType {
|
||||
guard let params = currentCall?.currentParams else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
if params.videoEnabled {
|
||||
return .video
|
||||
} else {
|
||||
return .audio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CoreDelegate
|
||||
extension LinphoneBridge: CoreDelegate {
|
||||
func onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState, message: String) {
|
||||
registrationState = state
|
||||
|
||||
switch state {
|
||||
case .Ok:
|
||||
isRegistered = true
|
||||
case .Failed, .None, .Progress:
|
||||
isRegistered = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) {
|
||||
callState = state
|
||||
|
||||
switch state {
|
||||
case .IncomingReceived:
|
||||
currentCall = call
|
||||
print("onCallStateChanged: INCOMING_RECEIVED \(call)")
|
||||
|
||||
case .Connected:
|
||||
print("onCallStateChanged: CONNECTED \(call)")
|
||||
|
||||
if let remoteView = remoteViewAcquisitor() {
|
||||
call.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(remoteView).toOpaque())
|
||||
} else {
|
||||
print("onCallStateChanged: CONNECTED remoteViewAcquisitor found nothing")
|
||||
}
|
||||
|
||||
if let localView = localViewAcquisitor() {
|
||||
core.nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(localView).toOpaque())
|
||||
} else {
|
||||
print("onCallStateChanged: CONNECTED localViewAcquisitor found nothing")
|
||||
}
|
||||
|
||||
case .StreamsRunning:
|
||||
print("onCallStateChanged: STREAMS_RUNNING \(call)")
|
||||
|
||||
if let remoteView = remoteViewAcquisitor() {
|
||||
call.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(remoteView).toOpaque())
|
||||
} else {
|
||||
print("onCallStateChanged: STREAMS_RUNNING remoteViewAcquisitor found nothing")
|
||||
}
|
||||
|
||||
if let localView = localViewAcquisitor() {
|
||||
core.nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(localView).toOpaque())
|
||||
} else {
|
||||
print("onCallStateChanged: STREAMS_RUNNING localViewAcquisitor found nothing")
|
||||
}
|
||||
|
||||
case .End, .Error, .Released:
|
||||
currentCall = nil
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
27
ios/Classes/Views/LocalView.swift
Normal file
27
ios/Classes/Views/LocalView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Flutter
|
||||
import linphonesw
|
||||
|
||||
class LocalView: NSObject, FlutterPlatformView {
|
||||
private var _lvvh: LinphoneVideoViewHolder? = nil
|
||||
private var _view: UIView? = nil
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?,
|
||||
binaryMessenger messenger: FlutterBinaryMessenger?,
|
||||
cacher: (UIView) -> Void
|
||||
) {
|
||||
super.init()
|
||||
|
||||
_lvvh = LinphoneVideoViewHolder { view in
|
||||
self._view = view
|
||||
}
|
||||
|
||||
cacher(_view!)
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
return _view!
|
||||
}
|
||||
}
|
||||
32
ios/Classes/Views/LocalViewFactory.swift
Normal file
32
ios/Classes/Views/LocalViewFactory.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class LocalViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private var messenger: FlutterBinaryMessenger
|
||||
private var cacher: (UIView) -> Void
|
||||
|
||||
init(messenger: FlutterBinaryMessenger, cacher: @escaping (UIView) -> Void) {
|
||||
self.messenger = messenger
|
||||
self.cacher = cacher
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(
|
||||
withFrame frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?
|
||||
) -> FlutterPlatformView {
|
||||
return LocalView(
|
||||
frame: frame,
|
||||
viewIdentifier: viewId,
|
||||
arguments: args,
|
||||
binaryMessenger: messenger,
|
||||
cacher: cacher
|
||||
)
|
||||
}
|
||||
|
||||
/// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`.
|
||||
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
}
|
||||
27
ios/Classes/Views/RemoteView.swift
Normal file
27
ios/Classes/Views/RemoteView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Flutter
|
||||
import linphonesw
|
||||
|
||||
class RemoteView: NSObject, FlutterPlatformView {
|
||||
private var _lvvh: LinphoneVideoViewHolder? = nil
|
||||
private var _view: UIView? = nil
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?,
|
||||
binaryMessenger messenger: FlutterBinaryMessenger?,
|
||||
cacher: (UIView) -> Void
|
||||
) {
|
||||
super.init()
|
||||
|
||||
_lvvh = LinphoneVideoViewHolder { view in
|
||||
self._view = view
|
||||
}
|
||||
|
||||
cacher(_view!)
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
return _view!
|
||||
}
|
||||
}
|
||||
32
ios/Classes/Views/RemoteViewFactory.swift
Normal file
32
ios/Classes/Views/RemoteViewFactory.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class RemoteViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private var messenger: FlutterBinaryMessenger
|
||||
private var cacher: (UIView) -> Void
|
||||
|
||||
init(messenger: FlutterBinaryMessenger, cacher: @escaping (UIView) -> Void) {
|
||||
self.messenger = messenger
|
||||
self.cacher = cacher
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(
|
||||
withFrame frame: CGRect,
|
||||
viewIdentifier viewId: Int64,
|
||||
arguments args: Any?
|
||||
) -> FlutterPlatformView {
|
||||
return RemoteView(
|
||||
frame: frame,
|
||||
viewIdentifier: viewId,
|
||||
arguments: args,
|
||||
binaryMessenger: messenger,
|
||||
cacher: cacher
|
||||
)
|
||||
}
|
||||
|
||||
/// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`.
|
||||
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,18 @@
|
|||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'liblinphone_flutter'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new Flutter plugin project.'
|
||||
s.version = '0.0.2'
|
||||
s.summary = 'libLinPhone integration library for Flutter apps'
|
||||
s.description = <<-DESC
|
||||
A new Flutter plugin project.
|
||||
libLinPhone integration library for Flutter apps
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.homepage = 'https://git.nuark.xyz/nuark/liblinphone_flutter'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.author = { 'nuark' => 'me@nuark.xyz' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.ios.dependency 'linphone-sdk', '~> 5.2.0'
|
||||
s.platform = :ios, '13.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue