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
11
README.md
11
README.md
|
|
@ -1,15 +1,8 @@
|
||||||
# liblinphone_flutter
|
# liblinphone_flutter
|
||||||
|
|
||||||
A new Flutter plugin project.
|
libLinPhone integration library for Flutter apps
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
This project is a starting point for a Flutter
|
Better docs will be available some day
|
||||||
[plug-in package](https://flutter.dev/to/develop-plugins),
|
|
||||||
a specialized package that includes platform-specific implementation code for
|
|
||||||
Android and/or iOS.
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
|
||||||
[online documentation](https://docs.flutter.dev), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
// For more information about Flutter integration tests, please see
|
// For more information about Flutter integration tests, please see
|
||||||
// https://flutter.dev/to/integration-testing
|
// https://flutter.dev/to/integration-testing
|
||||||
|
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
|
@ -14,12 +13,4 @@ import 'package:liblinphone_flutter/liblinphone_flutter.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
|
|
||||||
final LiblinphoneFlutter plugin = LiblinphoneFlutter();
|
|
||||||
final String? version = await plugin.getPlatformVersion();
|
|
||||||
// The version string depends on the host platform running the test, so
|
|
||||||
// just assert that some non-empty string is returned.
|
|
||||||
expect(version?.isNotEmpty, true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
|
||||||
47
example/ios/Podfile
Normal file
47
example/ios/Podfile
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
source 'https://gitlab.linphone.org/BC/public/podspec.git'
|
||||||
|
# source 'https://github.com/CocoaPods/Specs.git'
|
||||||
|
|
||||||
|
platform :ios, '15.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'linphone-sdk', '~> 5.2.0'
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
36
example/ios/Podfile.lock
Normal file
36
example/ios/Podfile.lock
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
PODS:
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- integration_test (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- liblinphone_flutter (0.0.2):
|
||||||
|
- Flutter
|
||||||
|
- linphone-sdk (~> 5.2.0)
|
||||||
|
- linphone-sdk (5.2.114)
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
|
- liblinphone_flutter (from `.symlinks/plugins/liblinphone_flutter/ios`)
|
||||||
|
- linphone-sdk (~> 5.2.0)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
https://gitlab.linphone.org/BC/public/podspec.git:
|
||||||
|
- linphone-sdk
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
|
liblinphone_flutter:
|
||||||
|
:path: ".symlinks/plugins/liblinphone_flutter/ios"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
|
liblinphone_flutter: 704c79aa1453d588208c101dd5b54077cf8c1a44
|
||||||
|
linphone-sdk: dfe70cd91cd826e1a6833f349525a305f11b438c
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: d42999c5f93753f05e2705e1d227f627884c6aed
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
2C00FF212A990F1E8CE45C54 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09B022171C775101BE22CE5C /* Pods_Runner.framework */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
68507299350137909F454BC8 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386F909BD45ABFF6031B5212 /* Pods_RunnerTests.framework */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
|
@ -41,15 +43,20 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
09B022171C775101BE22CE5C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
2DF5D8DA1E5928622BD009EC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
386F909BD45ABFF6031B5212 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
4563D893A8FBB4C7E39A3A5F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
96B29D535737AA982E97F291 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -57,6 +64,9 @@
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
DE898C41210C52B8BADD2DA0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
DF039402BCC354CBC1FAAD84 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
F8FC3E5625514CBF8149E1E4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -64,6 +74,15 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
2C00FF212A990F1E8CE45C54 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
AD123D619E8E0E414D0D1F60 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
68507299350137909F454BC8 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -96,6 +115,8 @@
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
DB0867B7C53EA2362A9BFFB8 /* Pods */,
|
||||||
|
F6D6E356ADE3EF2BB2EC17CB /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -124,6 +145,29 @@
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB0867B7C53EA2362A9BFFB8 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DF039402BCC354CBC1FAAD84 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
F8FC3E5625514CBF8149E1E4 /* Pods-Runner.release.xcconfig */,
|
||||||
|
2DF5D8DA1E5928622BD009EC /* Pods-Runner.profile.xcconfig */,
|
||||||
|
DE898C41210C52B8BADD2DA0 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
4563D893A8FBB4C7E39A3A5F /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
96B29D535737AA982E97F291 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
F6D6E356ADE3EF2BB2EC17CB /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
09B022171C775101BE22CE5C /* Pods_Runner.framework */,
|
||||||
|
386F909BD45ABFF6031B5212 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -131,8 +175,10 @@
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
637AFEC2DA5353F588BE980A /* [CP] Check Pods Manifest.lock */,
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
AD123D619E8E0E414D0D1F60 /* Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
|
@ -148,12 +194,14 @@
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
FFE1AB77575F403F0AA47C18 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
6DA617089093A1E6CEDD66DC /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
|
@ -241,6 +289,45 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
|
637AFEC2DA5353F588BE980A /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
6DA617089093A1E6CEDD66DC /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
|
|
@ -256,6 +343,28 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
|
FFE1AB77575F403F0AA47C18 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|
@ -383,6 +492,7 @@
|
||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = DE898C41210C52B8BADD2DA0 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|
@ -400,6 +510,7 @@
|
||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 4563D893A8FBB4C7E39A3A5F /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|
@ -415,6 +526,7 @@
|
||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 96B29D535737AA982E97F291 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,7 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,7 @@ class _MyAppState extends State<MyApp> {
|
||||||
String platformVersion;
|
String platformVersion;
|
||||||
// Platform messages may fail, so we use a try/catch PlatformException.
|
// Platform messages may fail, so we use a try/catch PlatformException.
|
||||||
// We also handle the message potentially returning null.
|
// We also handle the message potentially returning null.
|
||||||
try {
|
platformVersion = 'stub';
|
||||||
platformVersion =
|
|
||||||
await _liblinphoneFlutterPlugin.getPlatformVersion() ?? 'Unknown platform version';
|
|
||||||
} on PlatformException {
|
|
||||||
platformVersion = 'Failed to get platform version.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the widget was removed from the tree while the asynchronous platform
|
// If the widget was removed from the tree while the asynchronous platform
|
||||||
// message was in flight, we want to discard the reply rather than calling
|
// message was in flight, we want to discard the reply rather than calling
|
||||||
|
|
@ -51,12 +46,8 @@ class _MyAppState extends State<MyApp> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: const Text('Plugin example app')),
|
||||||
title: const Text('Plugin example app'),
|
body: Center(child: Text('Running on: $_platformVersion\n')),
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Text('Running on: $_platformVersion\n'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
version: "0.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,261 @@ import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
|
public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
|
||||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
private var channel: FlutterMethodChannel!
|
||||||
let channel = FlutterMethodChannel(name: "liblinphone_flutter", binaryMessenger: registrar.messenger())
|
private var registrationEventsChannel: FlutterEventSink?
|
||||||
let instance = LiblinphoneFlutterPlugin()
|
private var callEventsChannel: FlutterEventSink?
|
||||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
private var activity: UIViewController?
|
||||||
switch call.method {
|
private var remoteViewCache: [Int: UIView] = [:]
|
||||||
case "getPlatformVersion":
|
private var localViewCache: [Int: UIView] = [:]
|
||||||
result("iOS " + UIDevice.current.systemVersion)
|
|
||||||
default:
|
private var linphoneBridge: LinphoneBridge!
|
||||||
result(FlutterMethodNotImplemented)
|
|
||||||
|
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|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'liblinphone_flutter'
|
s.name = 'liblinphone_flutter'
|
||||||
s.version = '0.0.1'
|
s.version = '0.0.2'
|
||||||
s.summary = 'A new Flutter plugin project.'
|
s.summary = 'libLinPhone integration library for Flutter apps'
|
||||||
s.description = <<-DESC
|
s.description = <<-DESC
|
||||||
A new Flutter plugin project.
|
libLinPhone integration library for Flutter apps
|
||||||
DESC
|
DESC
|
||||||
s.homepage = 'http://example.com'
|
s.homepage = 'https://git.nuark.xyz/nuark/liblinphone_flutter'
|
||||||
s.license = { :file => '../LICENSE' }
|
s.license = { :file => '../LICENSE' }
|
||||||
s.author = { 'Your Company' => 'email@example.com' }
|
s.author = { 'nuark' => 'me@nuark.xyz' }
|
||||||
s.source = { :path => '.' }
|
s.source = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*'
|
s.source_files = 'Classes/**/*'
|
||||||
s.dependency 'Flutter'
|
s.dependency 'Flutter'
|
||||||
|
s.ios.dependency 'linphone-sdk', '~> 5.2.0'
|
||||||
s.platform = :ios, '13.0'
|
s.platform = :ios, '13.0'
|
||||||
|
|
||||||
# Flutter.framework does not contain a i386 slice.
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: liblinphone_flutter
|
name: liblinphone_flutter
|
||||||
description: "A new Flutter plugin project."
|
description: "libLinPhone integration library for Flutter apps"
|
||||||
version: 0.0.1
|
version: 0.0.2
|
||||||
# homepage:
|
homepage: "https://git.nuark.xyz/nuark/liblinphone_flutter"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0-333.2.beta
|
sdk: ^3.9.0-333.2.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue