7 Commits

Author SHA1 Message Date
Ghislain MARY
dfad6c515f Add 02_IncomingCall qt tutorial. 2024-05-06 12:53:57 +02:00
Ghislain MARY
28c66b6398 Add 01_AccountLogin qt tutorial. 2024-05-06 12:53:57 +02:00
Ghislain MARY
53a8bbba8f Add 00_HelloWorld qt tutorial. 2024-04-29 18:49:19 +02:00
Simon Morlat
6c7585aac8 Improve CallKit tutorial, that was missing important information. 2023-06-14 05:54:25 +02:00
QuentinArguillere
3257bdb7ad Update incoming call tutorial to 5.2.66 SDK 2023-06-08 15:49:25 +02:00
Florent
c822edad28 fix event enum 2023-03-23 10:16:54 +01:00
Thibault Lemaire
aca479888a Give the UWP Tutorial a little refresher
In order to write an equivalent tutorial for Xamarin, I am first
following the UWP tutorial.

Fixed many little spelling mistakes and rephrased some sentences.

Fixed a crash when video is requested but the device has no camera.

Fixed a crash when opening an audio recording. (Linphone.Content.FilePath
returns a path with mixed '/' and '\'. I don't know why and I'm not sure
I understand why the file was auto-downloaded either)
2022-02-17 16:10:10 +01:00
117 changed files with 2479 additions and 1394 deletions

View File

@@ -74,7 +74,7 @@ struct ContentView: View {
HStack {
Text("Login State : ")
.font(.footnote)
Text(tutorialContext.loggedIn ? "Looged in" : "Unregistered")
Text(tutorialContext.loggedIn ? "Logged in" : "Unregistered")
.font(.footnote)
.foregroundColor(tutorialContext.loggedIn ? Color.green : Color.black)
}.padding(.top, 10.0)

View File

@@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git"
def basic_pods
if ENV['PODFILE_PATH'].nil?
pod 'linphone-sdk', '~> 5.0.48'
pod 'linphone-sdk', '~> 5.2.66'
else
pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk
end

View File

@@ -74,6 +74,11 @@ extension CallKitProviderDelegate: CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
do {
// The audio stream is going to start shortly: the AVAudioSession must be configured now.
// It is worth to note that an application does not have permission to configure the
// AVAudioSession outside of this delegate action while it is running in background,
// which is usually the case in an incoming call scenario.
tutorialContext.mCore.configureAudioSession();
try tutorialContext.mCall?.accept()
tutorialContext.isCallRunning = true
} catch {
@@ -83,17 +88,27 @@ extension CallKitProviderDelegate: CXProviderDelegate {
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
// This tutorial is not doing outgoing calls. If it had to do so,
// configureAudioSession() shall be called from here, just before launching the
// call.
// tutorialContext.mCore.configureAudioSession();
// tutorialContext.mCore.invite("sip:bob@example.net");
// action.fulfill();
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {}
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {}
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {}
func providerDidReset(_ provider: CXProvider) {}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
// The linphone Core must be notified that CallKit has activated the AVAudioSession
// in order to start streaming audio.
tutorialContext.mCore.activateAudioSession(actived: true)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
// The linphone Core must be notified that CallKit has deactivated the AVAudioSession.
tutorialContext.mCore.activateAudioSession(actived: false)
}
}

View File

@@ -1,418 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
6644275A274F952D00EF03AA /* AudioRouteInvestigationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66442759274F952D00EF03AA /* AudioRouteInvestigationApp.swift */; };
6644275C274F952D00EF03AA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6644275B274F952D00EF03AA /* ContentView.swift */; };
6644275E274F952F00EF03AA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6644275D274F952F00EF03AA /* Assets.xcassets */; };
66442761274F952F00EF03AA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66442760274F952F00EF03AA /* Preview Assets.xcassets */; };
EFDB99DC9232C7DED75C1132 /* Pods_AudioRouteInvestigation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82346E676A805A0558805BF9 /* Pods_AudioRouteInvestigation.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
57D00E4F6DE7380AAF3F0C47 /* Pods-AudioRouteInvestigation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AudioRouteInvestigation.release.xcconfig"; path = "Target Support Files/Pods-AudioRouteInvestigation/Pods-AudioRouteInvestigation.release.xcconfig"; sourceTree = "<group>"; };
66442756274F952D00EF03AA /* AudioRouteInvestigation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioRouteInvestigation.app; sourceTree = BUILT_PRODUCTS_DIR; };
66442759274F952D00EF03AA /* AudioRouteInvestigationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteInvestigationApp.swift; sourceTree = "<group>"; };
6644275B274F952D00EF03AA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
6644275D274F952F00EF03AA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
66442760274F952F00EF03AA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
82346E676A805A0558805BF9 /* Pods_AudioRouteInvestigation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AudioRouteInvestigation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8DBEF2EA75C5F7144D91A733 /* Pods-AudioRouteInvestigation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AudioRouteInvestigation.debug.xcconfig"; path = "Target Support Files/Pods-AudioRouteInvestigation/Pods-AudioRouteInvestigation.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
66442753274F952D00EF03AA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EFDB99DC9232C7DED75C1132 /* Pods_AudioRouteInvestigation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
23C37AA5C87C768D9E2DB7FB /* Pods */ = {
isa = PBXGroup;
children = (
8DBEF2EA75C5F7144D91A733 /* Pods-AudioRouteInvestigation.debug.xcconfig */,
57D00E4F6DE7380AAF3F0C47 /* Pods-AudioRouteInvestigation.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
6644274D274F952D00EF03AA = {
isa = PBXGroup;
children = (
66442758274F952D00EF03AA /* AudioRouteInvestigation */,
66442757274F952D00EF03AA /* Products */,
23C37AA5C87C768D9E2DB7FB /* Pods */,
6C18380DFC079682CAFAE959 /* Frameworks */,
);
sourceTree = "<group>";
};
66442757274F952D00EF03AA /* Products */ = {
isa = PBXGroup;
children = (
66442756274F952D00EF03AA /* AudioRouteInvestigation.app */,
);
name = Products;
sourceTree = "<group>";
};
66442758274F952D00EF03AA /* AudioRouteInvestigation */ = {
isa = PBXGroup;
children = (
66442759274F952D00EF03AA /* AudioRouteInvestigationApp.swift */,
6644275B274F952D00EF03AA /* ContentView.swift */,
6644275D274F952F00EF03AA /* Assets.xcassets */,
6644275F274F952F00EF03AA /* Preview Content */,
);
path = AudioRouteInvestigation;
sourceTree = "<group>";
};
6644275F274F952F00EF03AA /* Preview Content */ = {
isa = PBXGroup;
children = (
66442760274F952F00EF03AA /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
6C18380DFC079682CAFAE959 /* Frameworks */ = {
isa = PBXGroup;
children = (
82346E676A805A0558805BF9 /* Pods_AudioRouteInvestigation.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
66442755274F952D00EF03AA /* AudioRouteInvestigation */ = {
isa = PBXNativeTarget;
buildConfigurationList = 66442764274F952F00EF03AA /* Build configuration list for PBXNativeTarget "AudioRouteInvestigation" */;
buildPhases = (
59A6F8BC0224E743CE93CA1E /* [CP] Check Pods Manifest.lock */,
66442752274F952D00EF03AA /* Sources */,
66442753274F952D00EF03AA /* Frameworks */,
66442754274F952D00EF03AA /* Resources */,
6B7520AC43BE168C1B667379 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = AudioRouteInvestigation;
productName = AudioRouteInvestigation;
productReference = 66442756274F952D00EF03AA /* AudioRouteInvestigation.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
6644274E274F952D00EF03AA /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1310;
LastUpgradeCheck = 1310;
TargetAttributes = {
66442755274F952D00EF03AA = {
CreatedOnToolsVersion = 13.1;
};
};
};
buildConfigurationList = 66442751274F952D00EF03AA /* Build configuration list for PBXProject "AudioRouteInvestigation" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 6644274D274F952D00EF03AA;
productRefGroup = 66442757274F952D00EF03AA /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
66442755274F952D00EF03AA /* AudioRouteInvestigation */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
66442754274F952D00EF03AA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
66442761274F952F00EF03AA /* Preview Assets.xcassets in Resources */,
6644275E274F952F00EF03AA /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
59A6F8BC0224E743CE93CA1E /* [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-AudioRouteInvestigation-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;
};
6B7520AC43BE168C1B667379 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AudioRouteInvestigation/Pods-AudioRouteInvestigation-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AudioRouteInvestigation/Pods-AudioRouteInvestigation-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AudioRouteInvestigation/Pods-AudioRouteInvestigation-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
66442752274F952D00EF03AA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6644275C274F952D00EF03AA /* ContentView.swift in Sources */,
6644275A274F952D00EF03AA /* AudioRouteInvestigationApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
66442762274F952F00EF03AA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
66442763274F952F00EF03AA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
66442765274F952F00EF03AA /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8DBEF2EA75C5F7144D91A733 /* Pods-AudioRouteInvestigation.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"AudioRouteInvestigation/Preview Content\"";
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone access";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = BC.AudioRouteInvestigation;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
66442766274F952F00EF03AA /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 57D00E4F6DE7380AAF3F0C47 /* Pods-AudioRouteInvestigation.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"AudioRouteInvestigation/Preview Content\"";
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone access";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = BC.AudioRouteInvestigation;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
66442751274F952D00EF03AA /* Build configuration list for PBXProject "AudioRouteInvestigation" */ = {
isa = XCConfigurationList;
buildConfigurations = (
66442762274F952F00EF03AA /* Debug */,
66442763274F952F00EF03AA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
66442764274F952F00EF03AA /* Build configuration list for PBXNativeTarget "AudioRouteInvestigation" */ = {
isa = XCConfigurationList;
buildConfigurations = (
66442765274F952F00EF03AA /* Debug */,
66442766274F952F00EF03AA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 6644274E274F952D00EF03AA /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,98 +0,0 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,142 +0,0 @@
//
// AudioRouteInvestigationApp.swift
// AudioRouteInvestigation
//
// Created by QuentinArguillere on 25/11/2021.
//
import SwiftUI
import linphonesw
class AudioRouteInvestigation : ObservableObject
{
var mCore: Core!
var mCoreDelegate : CoreDelegate!
/* PLEASE FILL THESE FIELDS WITH YOUR SETTINGS */
var username : String = "user"
var passwd : String = "password"
var domain : String = "sip.linphone.org"
var remoteAddress : String = "sip:remote@sip.linphone.org"
@Published var loggedIn = false
@Published var callMsg : String = ""
@Published var isCallOutgoing : Bool = false
@Published var isCallIncoming : Bool = false
@Published var isCallRunning : Bool = false
@Published var isCallPaused = false
init() {
LoggingService.Instance.logLevel = LogLevel.Debug
try? mCore = Factory.Instance.createCore(configPath: "", factoryConfigPath: "", systemContext: nil)
//self.mCore.defaultOutputAudioDevice = self.mCore.audioDevices.first { $0.type == AudioDeviceType.Speaker }
mCoreDelegate = CoreDelegateStub( onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in
self.callMsg = message
if (state == .IncomingReceived) {
self.isCallIncoming = true
} else if (state == .OutgoingProgress) {
self.isCallOutgoing = true
} else if (state == .StreamsRunning) {
self.isCallOutgoing = false
self.isCallIncoming = false
self.isCallRunning = true
//self.mCore.outputAudioDevice = self.mCore.audioDevices.first { $0.type == AudioDeviceType.Speaker }
} else if (state == .Released) {
self.isCallOutgoing = false
self.isCallIncoming = false
self.isCallRunning = false
self.isCallPaused = false
}
}, onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
NSLog("New registration state is \(state) for user id \( String(describing: account.params?.identityAddress?.asString()))\n")
if (state == .Ok) {
self.loggedIn = true
} else if (state == .Cleared) {
self.loggedIn = false
}
})
mCore.addDelegate(delegate: mCoreDelegate)
try? mCore.start()
login()
}
func login() {
do {
let authInfo = try Factory.Instance.createAuthInfo(username: username, userid: "", passwd: passwd, ha1: "", realm: "", domain: domain)
let accountParams = try mCore.createAccountParams()
let identity = try Factory.Instance.createAddress(addr: String("sip:" + username + "@" + domain))
try! accountParams.setIdentityaddress(newValue: identity)
let address = try Factory.Instance.createAddress(addr: String("sip:" + domain))
try address.setTransport(newValue: TransportType.Tls)
try accountParams.setServeraddress(newValue: address)
accountParams.registerEnabled = true
let account = try mCore.createAccount(params: accountParams)
mCore.addAuthInfo(info: authInfo)
try mCore.addAccount(account: account)
mCore.defaultAccount = account
} catch { NSLog(error.localizedDescription) }
}
func outgoingCall() {
do {
let remoteAddress = try Factory.Instance.createAddress(addr: remoteAddress)
let params = try mCore.createCallParams(call: nil)
params.mediaEncryption = MediaEncryption.None
//mCore.defaultOutputAudioDevice = mCore.audioDevices.first { $0.type == AudioDeviceType.Speaker }
let call = mCore.inviteAddressWithParams(addr: remoteAddress, params: params)
//call?.outputAudioDevice = mCore.audioDevices.first { $0.type == AudioDeviceType.Speaker }
} catch { NSLog(error.localizedDescription) }
}
func incomingCall() {
try? mCore.currentCall?.accept()
}
func terminateCall() {
do {
if (mCore.callsNb == 0) { return }
let coreCall = (mCore.currentCall != nil) ? mCore.currentCall : mCore.calls[0]
if let call = coreCall {
try call.terminate()
}
} catch { NSLog(error.localizedDescription) }
}
func pauseOrResume() {
do {
if (mCore.callsNb == 0) { return }
let coreCall = (mCore.currentCall != nil) ? mCore.currentCall : mCore.calls[0]
if let call = coreCall {
if (call.state != Call.State.Paused && call.state != Call.State.Pausing) {
try call.pause()
isCallPaused = true
} else if (call.state != Call.State.Resuming) {
try call.resume()
isCallPaused = false
}
}
} catch { NSLog(error.localizedDescription) }
}
}
@main
struct AudioRouteInvestigationApp: App {
@ObservedObject var audioRouteInvestigation = AudioRouteInvestigation()
var body: some Scene {
WindowGroup {
ContentView(audioRouteInvestigation: audioRouteInvestigation)
}
}
}

View File

@@ -1,82 +0,0 @@
//
// ContentView.swift
// AudioRouteInvestigation
//
// Created by QuentinArguillere on 25/11/2021.
//
import SwiftUI
import linphonesw
struct ContentView: View {
@ObservedObject var audioRouteInvestigation : AudioRouteInvestigation
func callStateString() -> String {
if (audioRouteInvestigation.isCallOutgoing) {
return "Call Outgoing"
} else if (audioRouteInvestigation.isCallIncoming) {
return "Call Incoming"
} else if (audioRouteInvestigation.isCallRunning) {
return "Call running"
} else {
return "No Call"
}
}
var body: some View {
VStack {
VStack {
HStack {
Text(audioRouteInvestigation.username)
Text(audioRouteInvestigation.loggedIn ? "REGISTERED" : "UNREGISTERED").foregroundColor(audioRouteInvestigation.loggedIn ? Color.green : Color.red)
}
Button(action: {
if (self.audioRouteInvestigation.isCallIncoming) {
self.audioRouteInvestigation.incomingCall()
} else {
self.audioRouteInvestigation.outgoingCall()
}
}){
Text( self.audioRouteInvestigation.isCallIncoming ? "Accept Incoming Call" : "Start Outgoing Call")
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 340.0, height: 45.0)
.background(Color.gray)
}.disabled(audioRouteInvestigation.isCallRunning)
Button(action: audioRouteInvestigation.pauseOrResume) {
Text(audioRouteInvestigation.isCallPaused ? "Resume call" : "Pause call")
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 340.0, height: 42.0)
.background(Color.gray)
}.padding(.top, 50).disabled(!audioRouteInvestigation.isCallRunning)
Button(action: audioRouteInvestigation.terminateCall) {
Text( "Terminate call")
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 340.0, height: 42.0)
.background(Color.gray)
}.padding(.top, 50).disabled(!audioRouteInvestigation.isCallRunning)
HStack {
Text("Call state: ").font(.title3).underline()
Text(callStateString())
Spacer()
}
HStack {
Text("Current Call msg: ").font(.title3).underline()
Text(audioRouteInvestigation.callMsg)
Spacer()
}.padding(.top, 50)
}
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(audioRouteInvestigation: AudioRouteInvestigation())
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,24 +0,0 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '11.0'
source "https://gitlab.linphone.org/BC/public/podspec.git"
source "https://github.com/CocoaPods/Specs.git"
def basic_pods
if ENV['PODFILE_PATH'].nil?
pod 'linphone-sdk', '~> 5.0.53'
else
pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk
end
end
target 'AudioRouteInvestigation' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for AudioRouteInvestigation
basic_pods
end

1
qt/00_HelloWorld/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.22)
project(00_HelloWorld LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml Quick)
find_package(LinphoneCxx REQUIRED)
set(CMAKE_AUTOMOC ON)
SET(CMAKE_AUTOUIC ON)
set(SOURCES
"src/main.cpp"
"src/CoreManager.cpp"
)
set(QRC_RESOURCES resources.qrc)
set(QML_SOURCES)
file(STRINGS ${QRC_RESOURCES} QRC_RESOURCES_CONTENT)
foreach(line ${QRC_RESOURCES_CONTENT})
set(result)
string(REGEX REPLACE
"^[ \t]*<[ \t]*file[ \t]*>[ \t]*(.+\\.[a-z]+)[ \t]*<[ \t]*/[ \t]*file[ \t]*>[ \t]*$"
"\\1"
result
"${line}"
)
string(REGEX MATCH "\\.[a-z]+$" is_ui ${result})
if(NOT ${is_ui} STREQUAL "")
list(APPEND QML_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${result}")
endif()
endforeach()
add_executable(00_HelloWorld ${SOURCES} ${QML_SOURCES} ${QRC_RESOURCES})
target_include_directories(00_HelloWorld PRIVATE ${LINPHONECXX_INCLUDE_DIRS})
target_link_libraries(00_HelloWorld PRIVATE Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick ${LINPHONECXX_LIBRARIES})
set_target_properties(00_HelloWorld PROPERTIES AUTORCC ON)
set_target_properties(00_HelloWorld PROPERTIES
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)

View File

@@ -0,0 +1,15 @@
# Hello World tutorial
The purpose of this tutorial is to explain how to build a Qt app depending on the Linphone SDK and to create the `Core` object that all our APIs depends on.
The user interface will only display the `Core`'s version, but in the next tutorial you will learn how to use it to login your SIP account.
## How to build
In the following instructions, replace **<PATH-TO-SDK>** by the real path where your SDK is located, e.g. *~/projects/linphone-sdk/build-default/linphone-sdk/desktop/*
mkdir build
cd build
cmake .. -DCMAKE_PREFIX_PATH=<PATH-TO-SDK>
cmake --build .

View File

@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>ui/MainPage.qml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,32 @@
#include "CoreManager.hpp"
CoreManager *CoreManager::mInstance = nullptr;
CoreManager::CoreManager()
{
// Create a core from the factory.
mCore = linphone::Factory::get()->createCore("", "", nullptr);
}
CoreManager::~CoreManager()
{
mCore = nullptr;
}
void CoreManager::init()
{
if (mInstance)
return;
mInstance = new CoreManager();
}
CoreManager *CoreManager::getInstance()
{
return mInstance;
}
QString CoreManager::getVersion() const
{
// Get the version from the core.
return QString::fromStdString(mCore->getVersion());
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QtCore>
#include <linphone++/linphone.hh>
class CoreManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QString version READ getVersion CONSTANT)
public:
static void init();
static CoreManager *getInstance();
QString getVersion() const;
private:
CoreManager();
~CoreManager();
std::shared_ptr<linphone::Core> mCore = nullptr;
static CoreManager *mInstance;
};

View File

@@ -0,0 +1,28 @@
#include <QDir>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "CoreManager.hpp"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
app.setOrganizationName("Belledonne Communications");
app.setOrganizationDomain("belledonne-communications.com");
app.setApplicationName(QFileInfo(app.applicationFilePath()).baseName());
QQmlApplicationEngine engine;
engine.load(QUrl("qrc:/ui/MainPage.qml"));
if (engine.rootObjects().isEmpty())
qFatal("Unable to open main window.");
// Initialize the CoreManager singleton and add it to the Qml context.
CoreManager::init();
auto coreManager = CoreManager::getInstance();
QQmlContext *ctx = engine.rootContext();
ctx->setContextProperty("coreManager", coreManager);
return app.exec();
}

View File

@@ -0,0 +1,18 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
ApplicationWindow {
id: window
visible: true
title: "Hello World"
width: 640
height: 480
Text {
id: versionText
text: "Hello world, Linphone core version is " + coreManager.version // Get the version from the core manager.
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}

1
qt/01_AccountLogin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -0,0 +1,52 @@
cmake_minimum_required(VERSION 3.22)
project(01_AccountLogin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml Quick)
find_package(LinphoneCxx REQUIRED)
set(CMAKE_AUTOMOC ON)
SET(CMAKE_AUTOUIC ON)
set(SOURCES
"src/main.cpp"
"src/App.cpp"
"src/CoreHandler.cpp"
"src/CoreListener.cpp"
"src/CoreManager.cpp"
)
set(QRC_RESOURCES resources.qrc)
set(QML_SOURCES)
file(STRINGS ${QRC_RESOURCES} QRC_RESOURCES_CONTENT)
foreach(line ${QRC_RESOURCES_CONTENT})
set(result)
string(REGEX REPLACE
"^[ \t]*<[ \t]*file[ \t]*>[ \t]*(.+\\.[a-z]+)[ \t]*<[ \t]*/[ \t]*file[ \t]*>[ \t]*$"
"\\1"
result
"${line}"
)
string(REGEX MATCH "\\.[a-z]+$" is_ui ${result})
if(NOT ${is_ui} STREQUAL "")
list(APPEND QML_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${result}")
endif()
endforeach()
get_filename_component(SDK_PATH "${CMAKE_PREFIX_PATH}" REALPATH)
find_path(MSPLUGINS_PATH "plugins" PATH_SUFFIXES "lib/mediastreamer" "lib64/mediastreamer" REQUIRED)
set(MSPLUGINS_PATH "${MSPLUGINS_PATH}/plugins")
add_executable(01_AccountLogin ${SOURCES} ${QML_SOURCES} ${QRC_RESOURCES})
target_compile_definitions(01_AccountLogin PRIVATE "SDK_PATH=\"${SDK_PATH}\"" "MSPLUGINS_PATH=\"${MSPLUGINS_PATH}\"")
target_include_directories(01_AccountLogin PRIVATE ${LINPHONECXX_INCLUDE_DIRS})
target_link_libraries(01_AccountLogin PRIVATE Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick ${LINPHONECXX_LIBRARIES})
set_target_properties(01_AccountLogin PROPERTIES AUTORCC ON)
set_target_properties(01_AccountLogin PROPERTIES
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)

View File

@@ -0,0 +1,15 @@
# Account Login tutorial
This project will walk you through the different steps of logging in and out of a SIP account.
If you do not yet have a SIP account, please create one here : https://www.linphone.org/freesip/home
## How to build
In the following instructions, replace **<PATH-TO-SDK>** by the real path where your SDK is located, e.g. *~/projects/linphone-sdk/build-default/linphone-sdk/desktop/*
mkdir build
cd build
cmake .. -DCMAKE_PREFIX_PATH=<PATH-TO-SDK>
cmake --build .

View File

@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/">
<file>ui/MainPage.qml</file>
<file>ui/RegistrationPage.qml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,47 @@
#include <QDir>
#include <QQmlContext>
#include "App.hpp"
#include "CoreManager.hpp"
using namespace std;
using namespace linphone;
App::App(int &argc, char *argv[]) : QGuiApplication(argc, argv)
{
setOrganizationName("Belledonne Communications");
setOrganizationDomain("belledonne-communications.com");
setApplicationName(QFileInfo(applicationFilePath()).baseName());
}
App::~App()
{
}
void App::init()
{
registerTypes();
mEngine = new QQmlApplicationEngine();
mEngine->load(QUrl("qrc:/ui/MainPage.qml"));
if (mEngine->rootObjects().isEmpty())
qFatal("Unable to open main window.");
// Initialize the CoreManager singleton and add it to the Qml context.
CoreManager::init(this);
auto coreManager = CoreManager::getInstance();
QQmlContext *ctx = mEngine->rootContext();
ctx->setContextProperty("coreManager", coreManager);
}
void App::stop()
{
CoreManager::uninit();
}
void App::registerTypes()
{
qRegisterMetaType<string>();
qRegisterMetaType<RegistrationState>();
qRegisterMetaType<shared_ptr<Account>>();
}

View File

@@ -0,0 +1,25 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <linphone++/linphone.hh>
class App : public QGuiApplication
{
Q_OBJECT
public:
App(int &argc, char *argv[]);
~App();
void init();
void stop();
private:
void registerTypes();
QQmlApplicationEngine *mEngine = nullptr;
};
Q_DECLARE_METATYPE(std::string);
Q_DECLARE_METATYPE(linphone::RegistrationState);
Q_DECLARE_METATYPE(std::shared_ptr<linphone::Account>);

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include "CoreHandler.hpp"
#include "CoreListener.hpp"
#include "CoreManager.hpp"
CoreHandler::CoreHandler()
{
mCoreListener = std::make_shared<CoreListener>();
connectTo(mCoreListener.get());
}
void CoreHandler::setListener(std::shared_ptr<linphone::Core> core)
{
core->addListener(mCoreListener);
}
void CoreHandler::removeListener(std::shared_ptr<linphone::Core> core)
{
core->removeListener(mCoreListener);
}
void CoreHandler::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message)
{
emit registrationStateChanged(account, state, message);
}
void CoreHandler::onGlobalStateChanged(const std::shared_ptr<linphone::Core> &, linphone::GlobalState state, const std::string &message)
{
switch (state)
{
case linphone::GlobalState::On:
qInfo() << "Core is running " << QString::fromStdString(message);
break;
case linphone::GlobalState::Off:
qInfo() << "Core is stopped " << QString::fromStdString(message);
emit coreStopped();
break;
case linphone::GlobalState::Startup:
qInfo() << "Core is starting" << QString::fromStdString(message);
emit coreStarting();
break;
default:
break;
}
}
void CoreHandler::connectTo(CoreListener *listener)
{
connect(listener, &CoreListener::accountRegistrationStateChanged, this, &CoreHandler::onAccountRegistrationStateChanged);
connect(listener, &CoreListener::globalStateChanged, this, &CoreHandler::onGlobalStateChanged);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <linphone++/linphone.hh>
#include <QObject>
class CoreListener;
class CoreHandler : public QObject
{
Q_OBJECT
public:
CoreHandler();
void setListener(std::shared_ptr<linphone::Core> core);
void removeListener(std::shared_ptr<linphone::Core> core);
signals:
void coreStarting();
void coreStopped();
void registrationStateChanged(const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
public slots:
void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
void onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message);
private:
void connectTo(CoreListener *listener);
std::shared_ptr<CoreListener> mCoreListener;
};

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CoreListener.hpp"
CoreListener::CoreListener(QObject *parent) : QObject(parent)
{
}
void CoreListener::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message)
{
emit accountRegistrationStateChanged(core, account, state, message);
}
void CoreListener::onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message)
{
emit globalStateChanged(core, gstate, message);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <linphone++/linphone.hh>
#include <QObject>
class CoreListener : public QObject, public linphone::CoreListener
{
Q_OBJECT
public:
CoreListener(QObject *parent = nullptr);
virtual ~CoreListener() = default;
virtual void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message) override;
virtual void onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message) override;
signals:
void accountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
void globalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message);
};

View File

@@ -0,0 +1,235 @@
#include <QDebug>
#include "CoreHandler.hpp"
#include "CoreManager.hpp"
using namespace std;
using namespace linphone;
CoreManager *CoreManager::mInstance = nullptr;
CoreManager::CoreManager(QObject *parent) : QObject(parent)
{
mHandler = QSharedPointer<CoreHandler>::create();
CoreHandler *coreHandler = mHandler.get();
QObject::connect(coreHandler, &CoreHandler::coreStarting, this, &CoreManager::startIterate, Qt::QueuedConnection);
QObject::connect(coreHandler, &CoreHandler::coreStopped, this, &CoreManager::stopIterate, Qt::QueuedConnection);
QObject::connect(coreHandler, &CoreHandler::registrationStateChanged, this, &CoreManager::onRegistrationStateChanged, Qt::QueuedConnection);
// Delay the creation of the core so that the CoreManager instance is
// already set.
QTimer::singleShot(10, [this]()
{ createLinphoneCore(); });
}
CoreManager::~CoreManager()
{
mHandler->removeListener(mCore);
mHandler = nullptr;
mCore = nullptr;
}
void CoreManager::init(QObject *parent)
{
if (mInstance)
return;
mInstance = new CoreManager(parent);
}
void CoreManager::uninit()
{
if (mInstance)
{
mInstance->stopIterate();
auto core = mInstance->mCore;
delete mInstance;
mInstance = nullptr;
core->stop();
}
}
CoreManager *CoreManager::getInstance()
{
return mInstance;
}
void CoreManager::startIterate()
{
// Start a timer to call the core iterate every 20 ms.
mIterateTimer = new QTimer(this);
mIterateTimer->setInterval(20);
QObject::connect(mIterateTimer, &QTimer::timeout, this, &CoreManager::iterate);
qInfo() << QStringLiteral("Start iterate");
mIterateTimer->start();
}
void CoreManager::stopIterate()
{
qInfo() << QStringLiteral("Stop iterate");
mIterateTimer->stop();
mIterateTimer->deleteLater(); // Allow the timer to continue its stuff
mIterateTimer = nullptr;
}
void CoreManager::login(QString identity, QString password, QString transport)
{
if (mLoginButtonEnabled)
{
setProperty("loginButtonEnabled", false);
// To configure a SIP account, we need an Account object and an AuthInfo object
// The first one is how to connect to the proxy server, the second one stores the credentials
// Here we are creating an AuthInfo object from the identity Address and password provided by the user.
shared_ptr<Address> address = Factory::get()->createAddress(identity.toStdString());
// The AuthInfo can be created from the Factory as it's only a data class
// userID is set to null as it's the same as the username in our case
// ha1 is set to null as we are using the clear text password. Upon first register, the hash will be computed automatically.
// The realm will be determined automatically from the first register, as well as the algorithm
shared_ptr<AuthInfo> authInfo = Factory::get()->createAuthInfo(address->getUsername(), "", password.toStdString(), "", "", address->getDomain());
// And we add it to the Core
mCore->addAuthInfo(authInfo);
// Then we create an AccountParams object.
// It contains the account informations needed by the core
shared_ptr<AccountParams> accountParams = mCore->createAccountParams();
// A SIP account is identified by an identity address that we can construct from the username and domain
accountParams->setIdentityAddress(address);
// We also need to configure where the proxy server is located
shared_ptr<Address> serverAddr = Factory::get()->createAddress("sip:" + address->getDomain());
// We use the Address object to easily set the transport protocol
if (transport == "tls")
{
serverAddr->setTransport(TransportType::Tls);
}
else if (transport == "tcp")
{
serverAddr->setTransport(TransportType::Tcp);
}
else
{
serverAddr->setTransport(TransportType::Udp);
}
accountParams->setServerAddress(serverAddr);
// If enableRegister is set to true, when this account will be added to the core it will
// automatically try to connect.
accountParams->enableRegister(true);
// We can now create an Account object from the AccountParams ...
shared_ptr<Account> account = mCore->createAccount(accountParams);
// ... and add it to the core, launching the connection process.
mCore->addAccount(account);
// Also set the newly added account as default
mCore->setDefaultAccount(account);
}
}
void CoreManager::logout()
{
if (mLogoutButtonEnabled)
{
setProperty("logoutButtonEnabled", false);
// Setting enableRegister to false on a connected Account object will
// launch the logout action.
shared_ptr<Account> account = mCore->getDefaultAccount();
if (account)
{
// BUT BE CAREFUL : the Params of an account are read-only
// You MUST Clone it :
shared_ptr<AccountParams> accountParams = account->getParams()->clone();
// Then you can modify the clone :
accountParams->enableRegister(false);
// And finally setting the new Params value triggers the changes, here the logout.
account->setParams(accountParams);
}
}
}
void CoreManager::onRegistrationStateChanged(const shared_ptr<Account> &account, RegistrationState state, const string &message)
{
setProperty("registerText", QString::fromStdString("Your registration state is : " + message));
switch (state)
{
// If the Account was logged out, we clear the Core.
case RegistrationState::Cleared:
case RegistrationState::None:
mCore->clearAllAuthInfo();
mCore->clearAccounts();
logoutGuiChanges();
break;
case RegistrationState::Ok:
loginGuiChanges();
break;
case RegistrationState::Progress:
loginInProgressGuiChanges();
break;
case RegistrationState::Failed:
loginFailedGuiChanges();
break;
default:
break;
}
}
void CoreManager::logoutGuiChanges()
{
setProperty("loginButtonEnabled", true);
setProperty("logoutButtonEnabled", false);
setProperty("loginText", "You are logged out");
}
void CoreManager::loginFailedGuiChanges()
{
setProperty("loginButtonEnabled", true);
setProperty("logoutButtonEnabled", false);
setProperty("loginText", "Login failed, try again");
}
void CoreManager::loginGuiChanges()
{
setProperty("loginButtonEnabled", false);
setProperty("logoutButtonEnabled", true);
setProperty("loginText", QString::fromStdString("You are logged in, with identity " + mCore->getIdentity() + "."));
}
void CoreManager::loginInProgressGuiChanges()
{
setProperty("loginButtonEnabled", false);
setProperty("logoutButtonEnabled", false);
setProperty("loginText", QString::fromStdString("Login in progress, with identity " + mCore->getIdentity() + "."));
}
void CoreManager::createLinphoneCore()
{
// Setting linphone log level to message.
auto loggingService = LoggingService::get();
loggingService->setLogLevel(LogLevel::Message);
// Configure paths.
string assetsPath = string(SDK_PATH) + "/share";
Factory::get()->setTopResourcesDir(assetsPath);
Factory::get()->setDataResourcesDir(assetsPath);
Factory::get()->setSoundResourcesDir(assetsPath + "/sounds/linphone");
Factory::get()->setRingResourcesDir(Factory::get()->getSoundResourcesDir() + "/rings");
Factory::get()->setImageResourcesDir(assetsPath + "/images");
Factory::get()->setMspluginsDir(MSPLUGINS_PATH);
// Create a core from the factory.
mCore = Factory::get()->createCore("", "", nullptr);
mCore->setRootCa(assetsPath + "/linphone/rootca.pem");
// Listen for core events.
mHandler->setListener(mCore);
// Start the core.
mCore->start();
}
void CoreManager::iterate()
{
if (mCore)
mCore->iterate();
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <QObject>
#include <QSharedPointer>
#include <QTimer>
#include <linphone++/linphone.hh>
class CoreHandler;
class CoreManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QString loginText MEMBER mLoginText NOTIFY loginTextChanged)
Q_PROPERTY(QString registerText MEMBER mRegisterText NOTIFY registerTextChanged)
Q_PROPERTY(bool loginButtonEnabled MEMBER mLoginButtonEnabled NOTIFY loginButtonEnabledChanged)
Q_PROPERTY(bool logoutButtonEnabled MEMBER mLogoutButtonEnabled NOTIFY logoutButtonEnabledChanged)
public:
static void init(QObject *parent);
static void uninit();
static CoreManager *getInstance();
signals:
void loginTextChanged(QString);
void registerTextChanged(QString);
void loginButtonEnabledChanged(bool);
void logoutButtonEnabledChanged(bool);
public slots:
void startIterate();
void stopIterate();
void login(QString identity, QString password, QString transport);
void logout();
void onRegistrationStateChanged(const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
private:
CoreManager(QObject *parent);
~CoreManager();
void logoutGuiChanges();
void loginFailedGuiChanges();
void loginGuiChanges();
void loginInProgressGuiChanges();
void createLinphoneCore();
void iterate();
std::shared_ptr<linphone::Core> mCore = nullptr;
QSharedPointer<CoreHandler> mHandler;
QTimer *mIterateTimer = nullptr;
QString mLoginText;
QString mRegisterText;
bool mLoginButtonEnabled = true;
bool mLogoutButtonEnabled = false;
static CoreManager *mInstance;
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QCoreApplication>
#include "App.hpp"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
App app(argc, argv);
app.init();
app.exec();
app.stop();
}

View File

@@ -0,0 +1,19 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ApplicationWindow {
id: window
visible: true
title: "Account Login"
width: 640
height: 480
// Main content
Loader {
id: contentLoader
anchors.fill: parent
source: 'qrc:/ui/RegistrationPage.qml'
}
}

View File

@@ -0,0 +1,95 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.fillHeight: true
GridLayout {
Layout.fillWidth: true
Layout.margins: 20
columnSpacing: 20
columns: 2
Text {
text: "Identity:"
}
TextField {
id: identityTextField
Layout.fillWidth: true
text: "sip:"
}
Text {
text: "Password:"
}
TextField {
id: passwordTextField
echoMode: TextInput.Password
Layout.fillWidth: true
placeholderText: "my password"
}
Text {
text: "Transport:"
}
RowLayout {
Layout.fillWidth: true
RadioButton {
id: tlsButton
text: "TLS"
checked: true
}
RadioButton {
id: tcpButton
text: "TCP"
}
RadioButton {
id: udpButton
text: "UDP"
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: 20
Button {
id: loginButton
text: "Login"
enabled: coreManager.loginButtonEnabled && identityTextField.text.length != 0 && passwordTextField.text.length != 0
onClicked: {
var transport = "tls"
if (tcpButton.checked) { transport = "tcp" }
else if (udpButton.checked) { transport = "udp" }
coreManager.login(identityTextField.text, passwordTextField.text, transport)
}
}
Button {
id: logoutButton
text: "Logout"
enabled: coreManager.logoutButtonEnabled
onClicked: {
coreManager.logout()
}
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Text {
id: loginText
text: coreManager.loginText
}
Text {
id: registrationText
text: coreManager.registerText
}
}
}

1
qt/02_IncomingCall/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -0,0 +1,52 @@
cmake_minimum_required(VERSION 3.22)
project(02_IncomingCall LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml Quick)
find_package(LinphoneCxx REQUIRED)
set(CMAKE_AUTOMOC ON)
SET(CMAKE_AUTOUIC ON)
set(SOURCES
"src/main.cpp"
"src/App.cpp"
"src/CoreHandler.cpp"
"src/CoreListener.cpp"
"src/CoreManager.cpp"
)
set(QRC_RESOURCES resources.qrc)
set(QML_SOURCES)
file(STRINGS ${QRC_RESOURCES} QRC_RESOURCES_CONTENT)
foreach(line ${QRC_RESOURCES_CONTENT})
set(result)
string(REGEX REPLACE
"^[ \t]*<[ \t]*file[ \t]*>[ \t]*(.+\\.[a-z]+)[ \t]*<[ \t]*/[ \t]*file[ \t]*>[ \t]*$"
"\\1"
result
"${line}"
)
string(REGEX MATCH "\\.[a-z]+$" is_ui ${result})
if(NOT ${is_ui} STREQUAL "")
list(APPEND QML_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${result}")
endif()
endforeach()
get_filename_component(SDK_PATH "${CMAKE_PREFIX_PATH}" REALPATH)
find_path(MSPLUGINS_PATH "plugins" PATH_SUFFIXES "lib/mediastreamer" "lib64/mediastreamer" REQUIRED)
set(MSPLUGINS_PATH "${MSPLUGINS_PATH}/plugins")
add_executable(02_IncomingCall ${SOURCES} ${QML_SOURCES} ${QRC_RESOURCES})
target_compile_definitions(02_IncomingCall PRIVATE "SDK_PATH=\"${SDK_PATH}\"" "MSPLUGINS_PATH=\"${MSPLUGINS_PATH}\"")
target_include_directories(02_IncomingCall PRIVATE ${LINPHONECXX_INCLUDE_DIRS})
target_link_libraries(02_IncomingCall PRIVATE Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick ${LINPHONECXX_LIBRARIES})
set_target_properties(02_IncomingCall PROPERTIES AUTORCC ON)
set_target_properties(02_IncomingCall PROPERTIES
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)

View File

@@ -0,0 +1,15 @@
# Incoming call tutorial
This time we are going to receive our first calls!
If you don't have SIP friends to test with, you can also install Linphone on your mobile device (Android or iOS) and call yourself with a different account.
## How to build
In the following instructions, replace **<PATH-TO-SDK>** by the real path where your SDK is located, e.g. *~/projects/linphone-sdk/build-default/linphone-sdk/desktop/*
mkdir build
cd build
cmake .. -DCMAKE_PREFIX_PATH=<PATH-TO-SDK>
cmake --build .

View File

@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>ui/MainPage.qml</file>
<file>ui/CallPage.qml</file>
<file>ui/RegistrationPage.qml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,49 @@
#include <QDir>
#include <QQmlContext>
#include "App.hpp"
#include "CoreManager.hpp"
using namespace std;
using namespace linphone;
App::App(int &argc, char *argv[]) : QGuiApplication(argc, argv)
{
setOrganizationName("Belledonne Communications");
setOrganizationDomain("belledonne-communications.com");
setApplicationName(QFileInfo(applicationFilePath()).baseName());
}
App::~App()
{
}
void App::init()
{
registerTypes();
mEngine = new QQmlApplicationEngine();
mEngine->load(QUrl("qrc:/ui/MainPage.qml"));
if (mEngine->rootObjects().isEmpty())
qFatal("Unable to open main window.");
// Initialize the CoreManager singleton and add it to the Qml context.
CoreManager::init(this);
auto coreManager = CoreManager::getInstance();
QQmlContext *ctx = mEngine->rootContext();
ctx->setContextProperty("coreManager", coreManager);
}
void App::stop()
{
CoreManager::uninit();
}
void App::registerTypes()
{
qRegisterMetaType<string>();
qRegisterMetaType<RegistrationState>();
qRegisterMetaType<shared_ptr<Account>>();
qRegisterMetaType<Call::State>();
qRegisterMetaType<shared_ptr<Call>>();
}

View File

@@ -0,0 +1,27 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <linphone++/linphone.hh>
class App : public QGuiApplication
{
Q_OBJECT
public:
App(int &argc, char *argv[]);
~App();
void init();
void stop();
private:
void registerTypes();
QQmlApplicationEngine *mEngine = nullptr;
};
Q_DECLARE_METATYPE(std::string);
Q_DECLARE_METATYPE(linphone::RegistrationState);
Q_DECLARE_METATYPE(std::shared_ptr<linphone::Account>);
Q_DECLARE_METATYPE(linphone::Call::State);
Q_DECLARE_METATYPE(std::shared_ptr<linphone::Call>);

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include "CoreHandler.hpp"
#include "CoreListener.hpp"
#include "CoreManager.hpp"
CoreHandler::CoreHandler()
{
mCoreListener = std::make_shared<CoreListener>();
connectTo(mCoreListener.get());
}
void CoreHandler::setListener(std::shared_ptr<linphone::Core> core)
{
core->addListener(mCoreListener);
}
void CoreHandler::removeListener(std::shared_ptr<linphone::Core> core)
{
core->removeListener(mCoreListener);
}
void CoreHandler::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message)
{
emit registrationStateChanged(account, state, message);
}
void CoreHandler::onCallStateStateChanged(const std::shared_ptr<linphone::Core> &, const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message)
{
emit callStateChanged(call, state, message);
}
void CoreHandler::onGlobalStateChanged(const std::shared_ptr<linphone::Core> &, linphone::GlobalState state, const std::string &message)
{
switch (state)
{
case linphone::GlobalState::On:
qInfo() << "Core is running " << QString::fromStdString(message);
break;
case linphone::GlobalState::Off:
qInfo() << "Core is stopped " << QString::fromStdString(message);
emit coreStopped();
break;
case linphone::GlobalState::Startup:
qInfo() << "Core is starting" << QString::fromStdString(message);
emit coreStarting();
break;
default:
break;
}
}
void CoreHandler::connectTo(CoreListener *listener)
{
connect(listener, &CoreListener::accountRegistrationStateChanged, this, &CoreHandler::onAccountRegistrationStateChanged);
connect(listener, &CoreListener::callStateChanged, this, &CoreHandler::onCallStateStateChanged);
connect(listener, &CoreListener::globalStateChanged, this, &CoreHandler::onGlobalStateChanged);
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <linphone++/linphone.hh>
#include <QObject>
class CoreListener;
class CoreHandler : public QObject
{
Q_OBJECT
public:
CoreHandler();
void setListener(std::shared_ptr<linphone::Core> core);
void removeListener(std::shared_ptr<linphone::Core> core);
signals:
void coreStarting();
void coreStopped();
void callStateChanged(const std::shared_ptr<linphone::Call>, linphone::Call::State state, const std::string &message);
void registrationStateChanged(const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
public slots:
void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
void onCallStateStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message);
void onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message);
private:
void connectTo(CoreListener *listener);
std::shared_ptr<CoreListener> mCoreListener;
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CoreListener.hpp"
CoreListener::CoreListener(QObject *parent) : QObject(parent)
{
}
void CoreListener::onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message)
{
emit accountRegistrationStateChanged(core, account, state, message);
}
void CoreListener::onCallStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message)
{
emit callStateChanged(core, call, state, message);
}
void CoreListener::onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message)
{
emit globalStateChanged(core, gstate, message);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <linphone++/linphone.hh>
#include <QObject>
class CoreListener : public QObject, public linphone::CoreListener
{
Q_OBJECT
public:
CoreListener(QObject *parent = nullptr);
virtual ~CoreListener() = default;
virtual void onAccountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message) override;
virtual void onCallStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message) override;
virtual void onGlobalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message) override;
signals:
void accountRegistrationStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
void callStateChanged(const std::shared_ptr<linphone::Core> &core, const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message);
void globalStateChanged(const std::shared_ptr<linphone::Core> &core, linphone::GlobalState gstate, const std::string &message);
};

View File

@@ -0,0 +1,285 @@
#include <QDebug>
#include "CoreHandler.hpp"
#include "CoreManager.hpp"
using namespace std;
using namespace linphone;
CoreManager *CoreManager::mInstance = nullptr;
CoreManager::CoreManager(QObject *parent) : QObject(parent)
{
mPage = QString("qrc:/ui/RegistrationPage.qml");
mHeaderText = QString("Registration Form");
mSoundButtonText = QString("Switch off sound");
mMicrophoneButtonText = QString("Mute");
mHandler = QSharedPointer<CoreHandler>::create();
CoreHandler *coreHandler = mHandler.get();
QObject::connect(coreHandler, &CoreHandler::coreStarting, this, &CoreManager::startIterate, Qt::QueuedConnection);
QObject::connect(coreHandler, &CoreHandler::coreStopped, this, &CoreManager::stopIterate, Qt::QueuedConnection);
QObject::connect(coreHandler, &CoreHandler::callStateChanged, this, &CoreManager::onCallStateChanged, Qt::QueuedConnection);
QObject::connect(coreHandler, &CoreHandler::registrationStateChanged, this, &CoreManager::onRegistrationStateChanged, Qt::QueuedConnection);
// Delay the creation of the core so that the CoreManager instance is
// already set.
QTimer::singleShot(10, [this]()
{ createLinphoneCore(); });
}
CoreManager::~CoreManager()
{
mHandler->removeListener(mCore);
mHandler = nullptr;
mCore = nullptr;
}
void CoreManager::init(QObject *parent)
{
if (mInstance)
return;
mInstance = new CoreManager(parent);
}
void CoreManager::uninit()
{
if (mInstance)
{
mInstance->stopIterate();
auto core = mInstance->mCore;
delete mInstance;
mInstance = nullptr;
core->stop();
}
}
CoreManager *CoreManager::getInstance()
{
return mInstance;
}
void CoreManager::startIterate()
{
// Start a timer to call the core iterate every 20 ms.
mIterateTimer = new QTimer(this);
mIterateTimer->setInterval(20);
QObject::connect(mIterateTimer, &QTimer::timeout, this, &CoreManager::iterate);
qInfo() << QStringLiteral("Start iterate");
mIterateTimer->start();
}
void CoreManager::stopIterate()
{
qInfo() << QStringLiteral("Stop iterate");
mIterateTimer->stop();
mIterateTimer->deleteLater(); // Allow the timer to continue its stuff
mIterateTimer = nullptr;
}
void CoreManager::login(QString identity, QString password)
{
if (mLoginButtonEnabled)
{
setProperty("loginButtonEnabled", false);
shared_ptr<Address> address = Factory::get()->createAddress(identity.toStdString());
shared_ptr<AuthInfo> authInfo = Factory::get()->createAuthInfo(address->getUsername(), "", password.toStdString(), "", "", address->getDomain());
mCore->addAuthInfo(authInfo);
shared_ptr<AccountParams> accountParams = mCore->createAccountParams();
accountParams->setIdentityAddress(address);
string serverAddr = "sip:" + address->getDomain() + ";transport=tls";
accountParams->setServerAddr(serverAddr);
accountParams->enableRegister(true);
shared_ptr<Account> account = mCore->createAccount(accountParams);
mCore->addAccount(account);
mCore->setDefaultAccount(account);
}
}
void CoreManager::logout()
{
shared_ptr<Account> account = mCore->getDefaultAccount();
if (account)
{
shared_ptr<AccountParams> accountParams = account->getParams()->clone();
accountParams->enableRegister(false);
account->setParams(accountParams);
}
}
void CoreManager::onCallStateChanged(const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message)
{
setProperty("callStateText", QString::fromStdString("Your call state is: " + message));
switch (state)
{
case Call::State::IncomingReceived:
// When you receive a call the Call::State is incoming receive. By default you can only have one current call,
// so if a call is in progress or one is already ringing the second remote call will be decline with the reason
// "Busy". If you want to implement a multi call app you can increase Core::setMaxCalls.
// Here we store the incoming call reference so we can accept or decline the call on user input.
mIncomingCall = call;
// And we update the GUI to notify the user of the incoming call.
setProperty("incomingCallVisible", true);
setProperty("incomingCallText", QString::fromStdString(mIncomingCall->getRemoteAddress()->asString()));
break;
case Call::State::StreamsRunning:
// The StreamsRunning state is the default one during a call.
callInProgressGuiUpdates();
break;
case Call::State::Error:
case Call::State::End:
case Call::State::Released:
// By default after 30 seconds of ringing without accept or decline a call is
// automatically ended.
mIncomingCall = nullptr;
endingCallGuiUpdates();
break;
}
}
void CoreManager::onRegistrationStateChanged(const shared_ptr<Account> &account, RegistrationState state, const string &message)
{
setProperty("registerText", QString::fromStdString("Your registration state is : " + message));
switch (state)
{
// If the Account was logged out, we clear the Core.
case RegistrationState::Cleared:
case RegistrationState::None:
mCore->clearAllAuthInfo();
mCore->clearAccounts();
setProperty("loginButtonEnabled", true);
break;
case RegistrationState::Ok:
setProperty("loginButtonEnabled", false);
setProperty("headerText", QString::fromStdString("Hello " + mCore->getDefaultProxyConfig()->findAuthInfo()->getUsername()));
setProperty("page", QString("qrc:/ui/CallPage.qml"));
break;
case RegistrationState::Progress:
setProperty("loginButtonEnabled", false);
break;
case RegistrationState::Failed:
setProperty("loginButtonEnabled", true);
break;
default:
break;
}
}
void CoreManager::hangup()
{
// Simply call terminateAllCalls to hang out.
mCore->terminateAllCalls();
}
void CoreManager::soundButtonClicked()
{
if (toggleSpeaker())
{
setProperty("soundButtonText", QString("Switch on Sound"));
}
else
{
setProperty("soundButtonText", QString("Switch off Sound"));
}
}
void CoreManager::microphoneButtonClicked()
{
if (toggleMicrophone())
{
setProperty("microphoneButtonText", QString("Mute"));
}
else
{
setProperty("microphoneButtonText", QString("Unmute"));
}
}
void CoreManager::answer()
{
if (mIncomingCall)
{
// To accept a call only use the accept() method on the call object.
// If we wanted, we could create a CallParams object and answer using this object to make changes to the call configuration.
mIncomingCall->accept();
mIncomingCall = nullptr;
}
}
void CoreManager::decline()
{
if (mIncomingCall)
{
// You have to give a Reason to decline a call. This info is sent to the remote.
mIncomingCall->decline(Reason::Declined);
mIncomingCall = nullptr;
}
}
void CoreManager::createLinphoneCore()
{
// Setting linphone log level to message.
auto loggingService = LoggingService::get();
loggingService->setLogLevel(LogLevel::Message);
// Configure paths.
string assetsPath = string(SDK_PATH) + "/share";
Factory::get()->setTopResourcesDir(assetsPath);
Factory::get()->setDataResourcesDir(assetsPath);
Factory::get()->setSoundResourcesDir(assetsPath + "/sounds/linphone");
Factory::get()->setRingResourcesDir(Factory::get()->getSoundResourcesDir() + "/rings");
Factory::get()->setImageResourcesDir(assetsPath + "/images");
Factory::get()->setMspluginsDir(MSPLUGINS_PATH);
// Create a core from the factory.
mCore = Factory::get()->createCore("", "", nullptr);
mCore->setRootCa(assetsPath + "/linphone/rootca.pem");
// Listen for core events.
mHandler->setListener(mCore);
// Start the core.
mCore->start();
}
void CoreManager::iterate()
{
if (mCore)
mCore->iterate();
}
bool CoreManager::toggleSpeaker()
{
// Calling setSpeakerMuted(true) on a Call object disables the sound output of this call.
bool newValue = !mCore->getCurrentCall()->getSpeakerMuted();
mCore->getCurrentCall()->setSpeakerMuted(newValue);
return newValue;
}
bool CoreManager::toggleMicrophone()
{
// The following toggles the microphone, disabling completely / enabling the sound capture from the device microphone
bool newValue = !mCore->micEnabled();
mCore->enableMic(newValue);
return newValue;
}
void CoreManager::callInProgressGuiUpdates()
{
setProperty("incomingCallVisible", false);
setProperty("inCallButtonsEnabled", true);
}
void CoreManager::endingCallGuiUpdates()
{
setProperty("incomingCallVisible", false);
setProperty("inCallButtonsEnabled", false);
setProperty("soundButtonText", "Switch off Sound");
setProperty("microphoneButtonText", "Mute");
}

View File

@@ -0,0 +1,84 @@
#pragma once
#include <QObject>
#include <QSharedPointer>
#include <QTimer>
#include <linphone++/linphone.hh>
class CoreHandler;
class CoreManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QString page MEMBER mPage NOTIFY pageChanged);
Q_PROPERTY(QString headerText MEMBER mHeaderText NOTIFY headerTextChanged)
Q_PROPERTY(QString registerText MEMBER mRegisterText NOTIFY registerTextChanged)
Q_PROPERTY(bool loginButtonEnabled MEMBER mLoginButtonEnabled NOTIFY loginButtonEnabledChanged)
Q_PROPERTY(QString callStateText MEMBER mCallStateText NOTIFY callStateTextChanged)
Q_PROPERTY(bool incomingCallVisible MEMBER mIncomingCallVisible NOTIFY incomingCallVisibleChanged)
Q_PROPERTY(QString incomingCallText MEMBER mIncomingCallText NOTIFY incomingCallTextChanged)
Q_PROPERTY(bool inCallButtonsEnabled MEMBER mInCallButtonsEnabled NOTIFY inCallButtonsEnabledChanged)
Q_PROPERTY(QString soundButtonText MEMBER mSoundButtonText NOTIFY soundButtonTextChanged)
Q_PROPERTY(QString microphoneButtonText MEMBER mMicrophoneButtonText NOTIFY microphoneButtonTextChanged)
public:
static void init(QObject *parent);
static void uninit();
static CoreManager *getInstance();
signals:
void pageChanged(QString);
void headerTextChanged(QString);
void registerTextChanged(QString);
void loginButtonEnabledChanged(bool);
void callStateTextChanged(QString);
void incomingCallVisibleChanged(bool);
void incomingCallTextChanged(QString);
void inCallButtonsEnabledChanged(bool);
void soundButtonTextChanged(QString);
void microphoneButtonTextChanged(QString);
public slots:
void startIterate();
void stopIterate();
void login(QString identity, QString password);
void logout();
void onCallStateChanged(const std::shared_ptr<linphone::Call> &call, linphone::Call::State state, const std::string &message);
void onRegistrationStateChanged(const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state, const std::string &message);
void hangup();
void soundButtonClicked();
void microphoneButtonClicked();
void answer();
void decline();
private:
CoreManager(QObject *parent);
~CoreManager();
void createLinphoneCore();
void iterate();
bool toggleSpeaker();
bool toggleMicrophone();
void callInProgressGuiUpdates();
void endingCallGuiUpdates();
std::shared_ptr<linphone::Core> mCore = nullptr;
std::shared_ptr<linphone::Call> mIncomingCall = nullptr;
QSharedPointer<CoreHandler> mHandler;
QTimer *mIterateTimer = nullptr;
QString mPage;
QString mHeaderText;
QString mRegisterText;
bool mLoginButtonEnabled = true;
QString mCallStateText;
bool mIncomingCallVisible = false;
QString mIncomingCallText;
bool mInCallButtonsEnabled = false;
QString mSoundButtonText;
QString mMicrophoneButtonText;
static CoreManager *mInstance;
};

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QCoreApplication>
#include "App.hpp"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
App app(argc, argv);
app.init();
app.exec();
app.stop();
}

View File

@@ -0,0 +1,77 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
GridLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.fillHeight: true
columns: 1
ColumnLayout {
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Text {
text: coreManager.callStateText
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: 20
Button {
text: "Hang up"
enabled: coreManager.inCallButtonsEnabled
onClicked: {
coreManager.hangup()
}
}
Button {
text: coreManager.soundButtonText
enabled: coreManager.inCallButtonsEnabled
onClicked: {
coreManager.soundButtonClicked()
}
}
Button {
text: coreManager.microphoneButtonText
enabled: coreManager.inCallButtonsEnabled
onClicked: {
coreManager.microphoneButtonClicked()
}
}
}
GridLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Layout.margins: 20
columnSpacing: 20
columns: 2
visible: coreManager.incomingCallVisible
Text {
Layout.alignment: Qt.AlignHCenter
Layout.columnSpan: 2
text: "You have a call from: " + coreManager.incomingCallText
}
Button {
text: "Answer"
onClicked: {
coreManager.answer()
}
}
Button {
text: "Decline"
onClicked: {
coreManager.decline()
}
}
}
}

View File

@@ -0,0 +1,35 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ApplicationWindow {
id: window
visible: true
title: "Account Login"
width: 640
height: 480
header: Rectangle {
color: "lightgray";
height: 40;
width: window.width
Text {
text: coreManager.headerText
font.bold: true
font.capitalization: Font.AllUppercase
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
height: parent.height
}
}
// Main content
Loader {
id: contentLoader
anchors.fill: parent
source: coreManager.page
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ColumnLayout {
//Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.fillWidth: true
Layout.fillHeight: true
GridLayout {
Layout.fillWidth: true
Layout.margins: 20
columnSpacing: 20
columns: 2
Text {
text: "Identity:"
}
TextField {
id: identityTextField
Layout.fillWidth: true
text: "sip:"
}
Text {
text: "Password:"
}
TextField {
id: passwordTextField
echoMode: TextInput.Password
Layout.fillWidth: true
placeholderText: "my password"
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Button {
text: "Login"
enabled: coreManager.loginButtonEnabled && identityTextField.text.length !== 0 && passwordTextField.text.length !== 0
onClicked: {
coreManager.login(identityTextField.text, passwordTextField.text)
}
}
}
ColumnLayout {
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Text {
text: coreManager.registerText
}
}
}

13
uwp/cs/.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
indent_style = tabs
indent_size = 4
[*.cs]
# IDE0008: Use explicit type
csharp_style_var_when_type_is_apparent = true
# IDE0008: Use explicit type
csharp_style_var_for_built_in_types = true
# IDE0008: Use explicit type
csharp_style_var_elsewhere = true

View File

@@ -151,13 +151,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig">
<Link>.editorconfig</Link>
</None>
<None Include="Readme.md" />
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">

View File

@@ -63,16 +63,16 @@ namespace _00_HelloWorld
factory.MspluginsDir = ".";
// Your Core can use up to 2 configuration files, but that isn't mandatory.
// The third parameter is the application context, he isn't mandatory when working
// with UWP, he is mandatory in an Android context for example.
// The third parameter is the application context, which is *not* mandatory when working
// with UWP, but *is* mandatory in an Android context for example.
// You can now create your Core object :
Core core = factory.CreateCore("", "", IntPtr.Zero);
// Once you got your core you can start to do a lot of things.
// Once you have your core you can start to do a lot of things.
HelloText += Core.Version;
// You should store the Core to keep a reference on it at all times while your app is alive.
// A good solution for that is either subclass the Application object or create a Service.
// You should store the Core to keep a reference to it at all times while your app is alive.
// A good solution for that is to either subclass the Application object or create a Service.
StoredCore = core;
}

View File

@@ -1,19 +1,15 @@
Linphone X UWP tutorial 00_HelloWorld
======================================
The first tutorial is just here to display a hello world app with the current Linphone's version number.
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
The first tutorial is just a hello world app displaying the current SDK version number.
Main files :
```
00_HelloWorld
│ README.md : you are here
│ README.md : you are here
│ App.xaml(.cs) : Default Windows Application file, nothing special here
│ MainPage.xaml(.cs) : This is were the magic happen,
│ jump into this file to learn about Linphone core creation and how to display a hello world.
│ MainPage.xaml(.cs) : This is were the magic happens,
│ jump into this file to learn the basics of how to setup your app to use Linphone by creating the Core object.
└───Assets : default UWP app assets
│ LockScreenLogo.scale-200.png

View File

@@ -151,10 +151,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -66,17 +66,18 @@ namespace _01_AccountLogin
StoredCore = core;
// We need to indicate to the core where are stored the root ans user certificates, for future TLS exchange.
// We need to indicate to the core where to find the root and user certificates, for future TLS exchange.
StoredCore.RootCa = Path.Combine(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "share", "Linphone", "rootca.pem");
StoredCore.UserCertificatesPath = ApplicationData.Current.LocalFolder.Path;
// In this tutorials we are going to log in and our registration state will change.
// In this tutorial we are going to log in and our registration state will change.
// Here we show you how to register a delegate method called every time the
// on OnAccountRegistrationStateChanged callback is triggered.
// OnAccountRegistrationStateChanged callback is triggered.
StoredCore.Listener.OnAccountRegistrationStateChanged += OnAccountRegistrationStateChanged;
// Start the core after setup, and before everything else.
StoredCore.Start();
StoredCore.AutoIterateEnabled = true;
// The method Iterate must be permanently called on our core.
// The Iterate method runs all the waiting backgrounds tasks and poll networks notifications.
@@ -135,7 +136,17 @@ namespace _01_AccountLogin
// We also need to configure where the proxy server is located
Address serverAddr = Factory.Instance.CreateAddress("sip:" + address.Domain);
// We use the Address object to easily set the transport protocol
serverAddr.Transport = TlsRadio.IsChecked ?? false ? TransportType.Tls : TcpRadio.IsChecked ?? false ? TransportType.Tcp : TransportType.Udp;
if (TlsRadio.IsChecked == true) {
serverAddr.Transport = TransportType.Tls;
}
else if (TcpRadio.IsChecked == true)
{
serverAddr.Transport = TransportType.Tcp;
}
else
{
serverAddr.Transport = TransportType.Udp;
}
accountParams.ServerAddress = serverAddr;
// If RegisterEnabled is set to true, when this account will be added to the core it will
// automatically try to connect.

View File

@@ -1,17 +1,13 @@
Linphone X UWP tutorial 01_AccountLogin
================================
In this tutorial we present you the different steps to login and logout a SIP account.
This project will walk you through the different steps of logging in and out of a SIP account.
To first register an account go here : https://www.linphone.org/freesip/home
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
If you do not yet have a SIP account, please create one here : https://www.linphone.org/freesip/home
New/updated files to watch :
```
01_AccountLogin
│ MainPage.xaml(.cs) : This time this page is a minimalist login page and it display your login status.
Watch its code to understand how to login/out with LinphoneSDK.
│ MainPage.xaml(.cs) : This was changed to a minimalist login page and displays your login status.
take a look at the code to understand how to login/out with LinphoneSDK.
```

View File

@@ -169,10 +169,10 @@
<Version>2.1.13</Version>
</PackageReference>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -3,20 +3,16 @@
This time we are going to receive our first calls !
Because the architecture of the first two tutorials were a bit too simple for a larger app we moved things a bit.
All the code about the core (creation, iterate, log in...) is now in the class Service/CoreService.
The architecture of the first two tutorials was a bit simple for a larger app, so we moved things a bit.
All the core-related code (creation, iterate, log in...) is now in the class Service/CoreService.
The page LoginPage is updated and now redirects to a new page (NavigationRoot) this page only contains a NavigationView.
If you are note familiar with NavigationView you can take a look at [the NavigationView doc](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/navigationview),
The LoginPage now redirects to a new page (NavigationRoot) this page only contains a NavigationView.
If you are unfamiliar with NavigationView you can take a look at [the NavigationView doc](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/navigationview),
but this is not mandatory since it contains no Linphone code and is only here for navigation.
By default the NavigationView load the new page CallsPage (the only one for now), on this page you can answer or decline incoming calls.
By default the NavigationView loads the new CallsPage (the only one for now), on this page you can answer or decline incoming calls.
If you don't have SIP friends to make tests we recommend you to install Linphone on your mobile device (Android or iOS) and to make calls to yourself.
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
If you don't have SIP friends to test with, you can also install Linphone on your mobile device (Android or iOS) and call yourself with a different account.
New/updated files :
@@ -33,10 +29,10 @@ New/updated files :
└───Views :
│ │ CallsPage.xaml(.cs) : This is the new page where you can make calls.
│ │ This is where you will find the new Linphone's uses.
│ │ CallsPage.xaml(.cs) : This is the new page from which you can make calls.
│ │ Also contains new Linphone-related code.
│ │
│ │ LoginPage.xaml(.cs) : The same login page as the previous step, now in his own file.
│ │ LoginPage.xaml(.cs) : The same login page as the previous step, now in its own file.
│ │
│ │ NavigationRoot.xaml(.cs) : The new page containing the NavigationView and the main app Frame.

View File

@@ -148,9 +148,9 @@ namespace _02_IncomingCall.Service
/// <summary>
/// Mute/Unmute your microphone.
/// Set MicEnabled=false on the Core mute your microphone globally.
/// Setting MicEnabled=false on the Core mutes your microphone globally.
/// </summary>
public bool MicEnabledSwitch()
public bool ToggleMic()
{
// The following toggles the microphone, disabling completely / enabling the sound capture from the device microphone
return Core.MicEnabled = !Core.MicEnabled;
@@ -158,9 +158,9 @@ namespace _02_IncomingCall.Service
/// <summary>
/// Enable/Disable the speaker sound.
/// Set SpeakerMuted=true on a Call object to disable the sound of this call.
/// Setting SpeakerMuted=true on a Call object disables the sound output of this call.
/// </summary>
public bool SpeakerMutedSwitch()
public bool ToggleSpeaker()
{
return Core.CurrentCall.SpeakerMuted = !Core.CurrentCall.SpeakerMuted;
}

View File

@@ -25,7 +25,7 @@
<TextBlock x:Name="CallText" Text="Your call state is : Idle" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10">
<Button x:Name="HangOut" Content="Hang out" Click="HangOutClick" IsEnabled="False" />
<Button x:Name="HangUp" Content="Hang up" Click="OnHangUpClicked" IsEnabled="False" />
<Button x:Name="Sound" Content="Switch off Sound" Click="SoundClick" IsEnabled="False" />
<Button x:Name="Mic" Content="Mute" Click="MicClick" IsEnabled="False" />
</StackPanel>
@@ -34,7 +34,7 @@
<StackPanel Grid.Row="1" x:Name="IncomingCallStackPanel" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" Margin="10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="You have a call from :" />
<TextBlock x:Name="IncommingCallText" Text="" />
<TextBlock x:Name="IncomingCallText" Text="" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="Answer" Content="Answer" Click="AnswerClick" />

View File

@@ -29,7 +29,7 @@ namespace _02_IncomingCall.Views
{
private CoreService CoreService { get; } = CoreService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@@ -50,9 +50,9 @@ namespace _02_IncomingCall.Views
HelloText.Text += CoreService.Core.DefaultProxyConfig.FindAuthInfo().Username;
// On each stage of a call we want to update our GUI.
// The same way we did it for OnAccountRegistrationStateChanged we can register
// The same way we did for OnAccountRegistrationStateChanged we can register
// a delegate called every time the state of a call changed.
// Watch this.OnCallStateChanged for more details
// See this.OnCallStateChanged for more details
CoreService.AddOnCallStateChangedDelegate(OnCallStateChanged);
if (CoreService.Core.CurrentCall != null)
@@ -64,7 +64,7 @@ namespace _02_IncomingCall.Views
/// <summary>
/// Method called when the "Hang out" button is clicked.
/// </summary>
private void HangOutClick(object sender, RoutedEventArgs e)
private void OnHangUpClicked(object sender, RoutedEventArgs e)
{
// Simply call TerminateAllCalls to hang out.
// You could also do something like CoreService.Core.CurrentCall?.Terminate();
@@ -73,11 +73,11 @@ namespace _02_IncomingCall.Views
/// <summary>
/// Method called when the "Switch on/off" button is clicked.
/// Watch CoreService.SpeakerMutedSwitch for more info.
/// See CoreService.ToggleSpeaker for more info.
/// </summary>
private void SoundClick(object sender, RoutedEventArgs e)
{
if (CoreService.SpeakerMutedSwitch())
if (CoreService.ToggleSpeaker())
{
Sound.Content = "Switch on Sound";
}
@@ -89,11 +89,11 @@ namespace _02_IncomingCall.Views
/// <summary>
/// Method to mute/unmute your microphone.
/// Watch CoreService.MicEnabledSwitch for more info.
/// See CoreService.ToggleMic for more info.
/// </summary>
private void MicClick(object sender, RoutedEventArgs e)
{
if (CoreService.MicEnabledSwitch())
if (CoreService.ToggleMic())
{
Mic.Content = "Mute";
}
@@ -119,10 +119,10 @@ namespace _02_IncomingCall.Views
// "Busy". If you want to implement a multi call app you can increase Core.MaxCalls.
// Here we store the incoming call reference so we can accept or decline the call on user input, see AnswerClick
// and DeclineClick.
IncommingCall = call;
IncomingCall = call;
// And we update the GUI to notify the user of the incoming call.
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + IncommingCall.RemoteAddress.AsString();
IncomingCallText.Text = " " + IncomingCall.RemoteAddress.AsString();
break;
@@ -136,7 +136,7 @@ namespace _02_IncomingCall.Views
case CallState.Released:
// By default after 30 seconds of ringing without accept or decline a call is
// automatically ended.
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
break;
@@ -148,7 +148,7 @@ namespace _02_IncomingCall.Views
/// </summary>
private async void AnswerClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
// We call this method to pop the microphone permission window.
// If the permission was already granted for this app, no pop up
@@ -157,8 +157,8 @@ namespace _02_IncomingCall.Views
// To accept a call only use the Accept() method on the call object.
// If we wanted, we could create a CallParams object and answer using this object to make changes to the call configuration.
IncommingCall.Accept();
IncommingCall = null;
IncomingCall.Accept();
IncomingCall = null;
}
}
@@ -167,12 +167,12 @@ namespace _02_IncomingCall.Views
/// </summary>
private void DeclineClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
// You have do give a Reason to decline a call. This info is sent to the remote.
// You have to give a Reason to decline a call. This info is sent to the remote.
// See Linphone.Reason to see the full list.
IncommingCall.Decline(Reason.Declined);
IncommingCall = null;
IncomingCall.Decline(Reason.Declined);
IncomingCall = null;
}
}
@@ -182,7 +182,7 @@ namespace _02_IncomingCall.Views
private void EndingCallGuiUpdates()
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
HangOut.IsEnabled = false;
HangUp.IsEnabled = false;
Sound.IsEnabled = false;
Mic.IsEnabled = false;
Mic.Content = "Mute";
@@ -195,7 +195,7 @@ namespace _02_IncomingCall.Views
private void CallInProgressGuiUpdates()
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
Sound.IsEnabled = true;
Mic.IsEnabled = true;
}

View File

@@ -52,7 +52,7 @@ namespace _02_IncomingCall.Views
{
switch (e.SourcePageType)
{
case Type c when e.SourcePageType == typeof(CallsPage):
case Type _ when e.SourcePageType == typeof(CallsPage):
((NavigationViewItem)navview.MenuItems[0]).IsSelected = true;
break;
}
@@ -65,18 +65,17 @@ namespace _02_IncomingCall.Views
ContentDialog noSettingsDialog = new ContentDialog
{
Title = "No settings",
Content = "There is no settings in this little app",
Content = "There are no settings in this little app",
CloseButtonText = "OK"
};
ContentDialogResult result = await noSettingsDialog.ShowAsync();
_ = await noSettingsDialog.ShowAsync();
return;
}
string invokedItemValue = args.InvokedItem as string;
if (invokedItemValue != null && invokedItemValue.Contains("Calls"))
if (args.InvokedItem is string invokedItemValue && invokedItemValue.Contains("Calls"))
{
AppNavFrame.Navigate(typeof(CallsPage));
_ = AppNavFrame.Navigate(typeof(CallsPage));
}
}
@@ -90,7 +89,7 @@ namespace _02_IncomingCall.Views
ContentDialog signOutDialog = new ContentDialog
{
Title = "Sign out ?",
Content = "All your current calls and actions will be canceled, are you sure to continue ?",
Content = "All your current calls and actions will be canceled.",
PrimaryButtonText = "Sign out",
CloseButtonText = "Cancel"
};

View File

@@ -170,10 +170,10 @@
<Version>2.1.13</Version>
</PackageReference>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -3,6 +3,10 @@
This time we are going to make our first video calls.
Note the new ANGLE.WindowsStore package that was added. This is required for video rendering.
(If you restored NuGet packages for the solution as indicated in the parent Readme, it should
already be installed, and no additional action is needed on your side.)
New/updated files :
```
@@ -14,10 +18,10 @@ New/updated files :
│ │ Now updated with the ability to make video calls.
│ │
│ │ VideoService.cs : A singleton service which contains the code to render the video call
│ │ on SwapChainPanel, using OpenGL.
│ │ on a SwapChainPanel, using OpenGL.
└───Views :
│ │ CallsPage.xaml(.cs) : This is the page where you can make calls.
│ │ This is where you will find the new Linphone's uses.
│ │ Also contains new Linphone-related code.
```

View File

@@ -70,15 +70,13 @@ namespace _03_OutgoingCall.Service
core.RootCa = Path.Combine(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "share", "Linphone", "rootca.pem");
core.UserCertificatesPath = ApplicationData.Current.LocalFolder.Path;
// NEW!
VideoActivationPolicy videoActivationPolicy = factory.CreateVideoActivationPolicy();
videoActivationPolicy.AutomaticallyAccept = true;
videoActivationPolicy.AutomaticallyInitiate = false;
core.VideoActivationPolicy = videoActivationPolicy;
if (core.VideoSupported())
{
core.VideoCaptureEnabled = true;
}
core.VideoCaptureEnabled = core.VideoSupported();
core.UsePreviewWindow(true);
}
return core;
@@ -174,12 +172,12 @@ namespace _03_OutgoingCall.Service
Core.InviteAddress(address);
}
public bool MicEnabledSwitch()
public bool ToggleMic()
{
return Core.MicEnabled = !Core.MicEnabled;
}
public bool SpeakerMutedSwitch()
public bool ToggleSpeaker()
{
return Core.CurrentCall.SpeakerMuted = !Core.CurrentCall.SpeakerMuted;
}
@@ -187,11 +185,8 @@ namespace _03_OutgoingCall.Service
/// <summary>
/// Ask the peer of the current call to enable/disable the video call.
/// </summary>
public async Task<bool> CameraEnabledSwitchAsync()
public async Task<bool> ToggleCameraAsync()
{
// We call this method to pop up the webcam permission window.
// If the permission was already granted for this app, no pop up
// appears.
await OpenCameraPopup();
// Retrieving the current call
@@ -199,24 +194,25 @@ namespace _03_OutgoingCall.Service
// Core.createCallParams(call) create CallParams matching the Call parameters,
// here the current call. CallParams contains a variety of parameters like
// audio bandwidth limit, media encryption type... And if the video is enable
// audio bandwidth limit, media encryption type...< And if the video is enable
// or not.
CallParams param = core.CreateCallParams(call);
// Switch the current VideoEnableValue
bool newValue = !param.VideoEnabled;
param.VideoEnabled = newValue;
param.VideoDirection = MediaDirection.RecvOnly;
// Try to update the call parameters with those new CallParams.
// If the video switch from true to false the peer can't refuse to disable the video.
// If the video switch from false to true and the peer don't have videoActivationPolicy.AutomaticallyAccept = true
// you have to wait for him to accept the update. The Call status is "Updating" during this time.
// If the video switched from true to false the peer can't refuse to disable the video.
// If the video switched from false to true and the peer doesn't have videoActivationPolicy.AutomaticallyAccept = true
// you have to wait for them to accept the update. The Call status is "Updating" during this time.
call.Update(param);
return newValue;
}
private async Task OpenMicrophonePopup()
public async Task OpenMicrophonePopup()
{
AudioGraphSettings settings = new AudioGraphSettings(Windows.Media.Render.AudioRenderCategory.Media);
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
@@ -231,11 +227,18 @@ namespace _03_OutgoingCall.Service
private async Task OpenCameraPopup()
{
MediaCapture mediaCapture = new Windows.Media.Capture.MediaCapture();
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
MediaCapture mediaCapture = new MediaCapture();
try
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
}
catch (Exception e) when(e.Message.StartsWith("No capture devices are available."))
{
// Ignored. You can ask the remote party for video even if you don't have a camera.
}
mediaCapture.Dispose();
}
}

View File

@@ -39,7 +39,7 @@ namespace _03_OutgoingCall.Service
/// When you want to start the video rendering you need to link the SwapChainPanel surface to Linphone.
/// Core.NativePreviewWindowId for the preview surface and Core.CurrentCall.NativeVideoWindowId for the
/// remote webcam surface.
/// Simply doing this allow Linphone to render your preview and the remote camera if they are available.
/// Simply doing this allows Linphone to render your preview and the remote camera if they are available.
/// </summary>
public void StartVideoStream(SwapChainPanel main, SwapChainPanel preview)
{

View File

@@ -32,7 +32,7 @@
<TextBlock x:Name="CallText" Text="Your call state is : Idle" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10">
<Button x:Name="HangOut" Content="Hang out" Click="HangOutClick" IsEnabled="False" />
<Button x:Name="HangUp" Content="Hang up" Click="OnHangUpClicked" IsEnabled="False" />
<Button x:Name="Sound" Content="Switch off Sound" Click="SoundClick" IsEnabled="False" />
<Button x:Name="Camera" Content="Switch on Camera" Click="CameraClick" IsEnabled="False" />
<Button x:Name="Mic" Content="Mute" Click="MicClick" IsEnabled="False" />

View File

@@ -31,7 +31,7 @@ namespace _03_OutgoingCall.Views
private VideoService VideoService { get; } = VideoService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@@ -71,14 +71,14 @@ namespace _03_OutgoingCall.Views
CoreService.Call(UriToCall.Text);
}
private void HangOutClick(object sender, RoutedEventArgs e)
private void OnHangUpClicked(object sender, RoutedEventArgs e)
{
CoreService.Core.TerminateAllCalls();
}
private void SoundClick(object sender, RoutedEventArgs e)
{
if (CoreService.SpeakerMutedSwitch())
if (CoreService.ToggleSpeaker())
{
Sound.Content = "Switch on Sound";
}
@@ -90,22 +90,22 @@ namespace _03_OutgoingCall.Views
/// <summary>
/// Method to turn on/off the video call.
/// Watch CoreService.CameraEnabledSwitchAsync for more info.
/// See CoreService.ToggleCameraAsync for more info.
/// </summary>
private async void CameraClick(object sender, RoutedEventArgs e)
{
await CoreService.CameraEnabledSwitchAsync();
await CoreService.ToggleCameraAsync();
// After CoreService.CameraEnabledSwitchAsync the Call state is "Updating".
// After CoreService.ToggleCameraAsync the Call state is "Updating".
// We wait for the return of the "StreamsRunning" state to update the GUI
// according to the final consensus between callers.
Camera.Content = "Waiting for accept ...";
Camera.Content = "Waiting for remote party to accept ...";
Camera.IsEnabled = false;
}
private void MicClick(object sender, RoutedEventArgs e)
{
if (CoreService.MicEnabledSwitch())
if (CoreService.ToggleMic())
{
Mic.Content = "Mute";
}
@@ -121,24 +121,24 @@ namespace _03_OutgoingCall.Views
switch (state)
{
case CallState.IncomingReceived:
IncommingCall = call;
IncomingCall = call;
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + IncommingCall.RemoteAddress.AsString();
IncommingCallText.Text = " " + IncomingCall.RemoteAddress.AsString();
break;
// The different states a call goes through before your peer answers.
case CallState.OutgoingInit:
case CallState.OutgoingProgress:
case CallState.OutgoingRinging:
// Different states you go through when you start a call and before your peer answer.
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
break;
// The StreamsRunning state is the default one during a call.
case CallState.StreamsRunning:
// The UpdatedByRemote state is triggered when the call's parameters are updated
// for example when video is asked/removed by remote.
case CallState.UpdatedByRemote:
// The StreamsRunning state is the default one during a call.
// The UpdatedByRemote is triggered when the call's parameters are updated
// for example when video is asked/removed by remote.
CallInProgressGuiUpdates();
if (call.CurrentParams.VideoEnabled)
@@ -154,7 +154,7 @@ namespace _03_OutgoingCall.Views
case CallState.Error:
case CallState.End:
case CallState.Released:
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
VideoService.StopVideoStream();
@@ -162,21 +162,22 @@ namespace _03_OutgoingCall.Views
}
}
private void AnswerClick(object sender, RoutedEventArgs e)
private async void AnswerClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
IncommingCall.Accept();
IncommingCall = null;
await CoreService.OpenMicrophonePopup();
IncomingCall.Accept();
IncomingCall = null;
}
}
private void DeclineClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
IncommingCall.Decline(Reason.Declined);
IncommingCall = null;
IncomingCall.Decline(Reason.Declined);
IncomingCall = null;
}
}
@@ -194,7 +195,7 @@ namespace _03_OutgoingCall.Views
/// <summary>
/// Method to show the webcam grid and start rendering remote and preview webcam.
/// Watch VideoService and more specifically VideoService.StartVideoStream to
/// See VideoService and more specifically VideoService.StartVideoStream to
/// understand how to start the rendering on a SwapChainPanel.
/// </summary>
private void StartVideoAndUpdateGui()
@@ -209,7 +210,7 @@ namespace _03_OutgoingCall.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = true;
HangOut.IsEnabled = false;
HangUp.IsEnabled = false;
Sound.IsEnabled = false;
Camera.IsEnabled = false;
Mic.IsEnabled = false;
@@ -223,7 +224,7 @@ namespace _03_OutgoingCall.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = false;
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
Sound.IsEnabled = true;
Camera.IsEnabled = true;
Mic.IsEnabled = true;

View File

@@ -26,6 +26,9 @@ using Windows.UI.Xaml.Navigation;
namespace _03_OutgoingCall.Views
{
/// <summary>
/// Introduced in step 02 IncomingCall
/// </summary>
public sealed partial class NavigationRoot : Page
{
private CoreService CoreService { get; } = CoreService.Instance;
@@ -38,9 +41,6 @@ namespace _03_OutgoingCall.Views
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Only do an inital navigate the first time the page loads
// when we switch out of compactoverloadmode this will fire but we don't want to navigate because
// there is already a page loaded
if (!hasLoadedPreviously)
{
AppNavFrame.Navigate(typeof(CallsPage));
@@ -52,7 +52,7 @@ namespace _03_OutgoingCall.Views
{
switch (e.SourcePageType)
{
case Type c when e.SourcePageType == typeof(CallsPage):
case Type _ when e.SourcePageType == typeof(CallsPage):
((NavigationViewItem)navview.MenuItems[0]).IsSelected = true;
break;
}
@@ -65,18 +65,16 @@ namespace _03_OutgoingCall.Views
ContentDialog noSettingsDialog = new ContentDialog
{
Title = "No settings",
Content = "There is no settings in this little app",
Content = "There are no settings in this little app",
CloseButtonText = "OK"
};
ContentDialogResult result = await noSettingsDialog.ShowAsync();
_ = await noSettingsDialog.ShowAsync();
return;
}
string invokedItemValue = args.InvokedItem as string;
if (invokedItemValue != null && invokedItemValue.Contains("Calls"))
if (args.InvokedItem is string invokedItemValue && invokedItemValue.Contains("Calls"))
{
AppNavFrame.Navigate(typeof(CallsPage));
_ = AppNavFrame.Navigate(typeof(CallsPage));
}
}
@@ -90,7 +88,7 @@ namespace _03_OutgoingCall.Views
ContentDialog signOutDialog = new ContentDialog
{
Title = "Sign out ?",
Content = "All your current calls and actions will be canceled, are you sure to continue ?",
Content = "All your current calls and actions will be canceled.",
PrimaryButtonText = "Sign out",
CloseButtonText = "Cancel"
};

View File

@@ -185,10 +185,10 @@
<Version>2.1.13</Version>
</PackageReference>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -5,12 +5,7 @@ Second big step in this tutorial, we can now communicate in basic chat rooms.
In this part you are going to learn how to send and receive text messages over SIP using LinphoneSDK.
For our first step with ChatRoom we are going to create only one to one basic ChatRoom (no encryption, no ephemeral),
and for now the tutorial app will only support text message.
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
- ANGLE.WindowsStore (for video rendering, version 2.1.13 recommended)
and for now the tutorial app will only support text messages.
New/Updated files :
@@ -20,16 +15,16 @@ New/Updated files :
│ │ CoreService.cs : A singleton service which contains the Linphone.Core.
│ │ We added some code to create new chat rooms here.
│ │
│ │ NavigationService.cs : A small service used to keeps reference to current pages displayed.
│ │ NavigationService.cs : A small service used to keep references to pages currently displayed.
└───Views :
│ │
│ │ ChatPage.xaml(.cs) : This is the frame displayed when you select a chat room.
│ │ For now it's a simple page where you can send message and see your
│ │ For now it's a basic page where you can send messages and see your
│ │ conversation history.
│ │
│ │ ChatsPage.xaml(.cs) : In this page we list all the existing chat rooms. If you select
│ │ one of them a ChatPage is render. You can also create new ChatRoom here.
│ │ ChatsPage.xaml(.cs) : In this page we list all the existing chat rooms. When you select
│ │ one of them a ChatPage is rendered. You can also create new ChatRoom here.
│ │
│ │ NavigationRoot.xaml(.cs) : The navigation page, you can now navigate the ChatsPage !
│ │ NavigationRoot.xaml(.cs) : The navigation page, you can now navigate to the ChatsPage !
```

View File

@@ -75,11 +75,8 @@ namespace _04_BasicChat.Service
videoActivationPolicy.AutomaticallyInitiate = false;
core.VideoActivationPolicy = videoActivationPolicy;
if (core.VideoSupported())
{
core.VideoDisplayFilter = "MSOGL";
core.VideoCaptureEnabled = true;
}
core.VideoCaptureEnabled = core.VideoSupported();
core.UsePreviewWindow(true);
}
return core;
@@ -192,17 +189,17 @@ namespace _04_BasicChat.Service
Core.InviteAddress(address);
}
public bool MicEnabledSwitch()
public bool ToggleMic()
{
return Core.MicEnabled = !Core.MicEnabled;
}
public bool SpeakerMutedSwitch()
public bool ToggleSpeaker()
{
return Core.CurrentCall.SpeakerMuted = !Core.CurrentCall.SpeakerMuted;
}
public async Task<bool> CameraEnabledSwitchAsync()
public async Task<bool> ToggleCameraAsync()
{
await OpenCameraPopup();
@@ -256,7 +253,7 @@ namespace _04_BasicChat.Service
return Core.CreateChatRoom(chatRoomParams, localAdress, new[] { remoteAddress });
}
private async Task OpenMicrophonePopup()
public async Task OpenMicrophonePopup()
{
AudioGraphSettings settings = new AudioGraphSettings(Windows.Media.Render.AudioRenderCategory.Media);
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
@@ -271,11 +268,18 @@ namespace _04_BasicChat.Service
private async Task OpenCameraPopup()
{
MediaCapture mediaCapture = new Windows.Media.Capture.MediaCapture();
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
MediaCapture mediaCapture = new MediaCapture();
try
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
}
catch (Exception e) when (e.Message.StartsWith("No capture devices are available."))
{
// Ignored.
}
mediaCapture.Dispose();
}
}

View File

@@ -32,7 +32,7 @@
<TextBlock x:Name="CallText" Text="Your call state is : Idle" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10">
<Button x:Name="HangOut" Content="Hang out" Click="HangOutClick" IsEnabled="False" />
<Button x:Name="HangUp" Content="Hang up" Click="OnHangUpClicked" IsEnabled="False" />
<Button x:Name="Sound" Content="Switch off Sound" Click="SoundClick" IsEnabled="False" />
<Button x:Name="Camera" Content="Switch on Camera" Click="CameraClick" IsEnabled="False" />
<Button x:Name="Mic" Content="Mute" Click="MicClick" IsEnabled="False" />

View File

@@ -31,7 +31,7 @@ namespace _04_BasicChat.Views
private VideoService VideoService { get; } = VideoService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@@ -63,14 +63,14 @@ namespace _04_BasicChat.Views
CoreService.Call(UriToCall.Text);
}
private void HangOutClick(object sender, RoutedEventArgs e)
private void OnHangUpClicked(object sender, RoutedEventArgs e)
{
CoreService.Core.TerminateAllCalls();
}
private void SoundClick(object sender, RoutedEventArgs e)
{
if (CoreService.SpeakerMutedSwitch())
if (CoreService.ToggleSpeaker())
{
Sound.Content = "Switch on Sound";
}
@@ -82,14 +82,14 @@ namespace _04_BasicChat.Views
private async void CameraClick(object sender, RoutedEventArgs e)
{
await CoreService.CameraEnabledSwitchAsync();
await CoreService.ToggleCameraAsync();
Camera.Content = "Waiting for accept ...";
Camera.IsEnabled = false;
}
private void MicClick(object sender, RoutedEventArgs e)
{
if (CoreService.MicEnabledSwitch())
if (CoreService.ToggleMic())
{
Mic.Content = "Mute";
}
@@ -99,18 +99,19 @@ namespace _04_BasicChat.Views
}
}
private void AnswerClick(object sender, RoutedEventArgs e)
private async void AnswerClick(object sender, RoutedEventArgs e)
{
IncommingCall.Accept();
IncommingCall = null;
await CoreService.OpenMicrophonePopup();
IncomingCall.Accept();
IncomingCall = null;
}
private void DeclineClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
IncommingCall.Decline(Reason.Declined);
IncommingCall = null;
IncomingCall.Decline(Reason.Declined);
IncomingCall = null;
}
}
@@ -121,7 +122,7 @@ namespace _04_BasicChat.Views
{
case CallState.IncomingReceived:
IncommingCall = call;
IncomingCall = call;
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + call.RemoteAddress.AsString();
break;
@@ -130,7 +131,7 @@ namespace _04_BasicChat.Views
case CallState.OutgoingProgress:
case CallState.OutgoingRinging:
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
break;
case CallState.StreamsRunning:
@@ -151,7 +152,7 @@ namespace _04_BasicChat.Views
case CallState.End:
case CallState.Released:
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
VideoService.StopVideoStream();
break;
@@ -178,7 +179,7 @@ namespace _04_BasicChat.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = true;
HangOut.IsEnabled = false;
HangUp.IsEnabled = false;
Sound.IsEnabled = false;
Camera.IsEnabled = false;
Mic.IsEnabled = false;
@@ -192,7 +193,7 @@ namespace _04_BasicChat.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = false;
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
Sound.IsEnabled = true;
Camera.IsEnabled = true;
Mic.IsEnabled = true;

View File

@@ -40,7 +40,7 @@ namespace _04_BasicChat.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ChatRoom = ((ChatRoom)e.Parameter);
ChatRoom = (ChatRoom)e.Parameter;
// The ChatRoom also offers to register to some callbacks.
// One of them is OnMessageReceived, like the one we used
@@ -49,17 +49,17 @@ namespace _04_BasicChat.Views
// ChatRoom.
ChatRoom.Listener.OnMessageReceived += OnMessageReceived;
// The method GetHistory get all the ChatMessage you have
// The method GetHistory gets all the ChatMessage's you have
// in your local database for this ChatRoom. GetHistory(0)
// means everything but you can specify a max number of messages.
// means 'all of them', but you can specify a max number of messages.
foreach (ChatMessage chatMessage in ChatRoom.GetHistory(0))
{
// See AddMessage(ChatMessage chatMessage) to see how we display messages
AddMessage(chatMessage);
}
// Mark all the messages in th ChatRoom as read, if some messages
// weren't, this will trigger some read notifications to the remote.
// Mark all the messages in the ChatRoom as read, if some messages
// weren't, this will send read notifications to the remote.
ChatRoom.MarkAsRead();
// Only here to update display of unread message count on parent frames.
@@ -99,8 +99,8 @@ namespace _04_BasicChat.Views
TextBlock textBlock = new TextBlock();
// You can find a lot of information on a ChatMessage object.
// Here we used the IsOutgoing info to choose on each side
// of the frame the message should be displayed.
// Here we use the IsOutgoing info to choose which side
// of the frame the message should be displayed on.
if (chatMessage.IsOutgoing)
{
textBlock.HorizontalAlignment = HorizontalAlignment.Right;
@@ -110,13 +110,13 @@ namespace _04_BasicChat.Views
textBlock.HorizontalAlignment = HorizontalAlignment.Left;
}
// You can see we take the first element of the Contents list of our ChatMessage. To keep
// We take the first element of the Contents list of our ChatMessage. To keep
// it simple we assume that we only send simple text message, we will talk more about multipart
// messages and other types of messagse in the next step.
// For now we only handle chat messages with one content, so we can find our text in
// For now we only handle chat messages with a single content item, so we can find our text in
// chatMessage.Contents.First().Utf8Text.
// We used ["" +] because if the message is a file transfer for example the Utf8Text can be null.
textBlock.Text = "" + chatMessage.Contents.First().Utf8Text;
// We wrap in a dollar string because if the message is e.g. a file transfer, the Utf8Text can be null.
textBlock.Text = $"{chatMessage.Contents.First().Utf8Text}";
MessagesList.Children.Add(textBlock);
@@ -136,8 +136,7 @@ namespace _04_BasicChat.Views
{
if (ChatRoom != null && OutgoingMessageText.Text != null && OutgoingMessageText.Text.Length > 0)
{
// We use the ChatRoom to create a new ChatMessage object. Here we used
// the method CreateMessage(string message) to create a text message.
// We use the ChatRoom to create a new ChatMessage object.
ChatMessage chatMessage = ChatRoom.CreateMessage(OutgoingMessageText.Text);
// And simply call the Send() method to send the message.

View File

@@ -51,8 +51,8 @@ namespace _04_BasicChat.Views
// Here we want to update the list every time a message is
// received (list order and unread message count, see ChatsPage.xaml)
// or sent (list order)
CoreService.AddOnOnMessageReceivedDelegate(OnMessageReceiveOrSent);
CoreService.AddOnMessageSentDelegate(OnMessageReceiveOrSent);
CoreService.AddOnOnMessageReceivedDelegate(OnMessageReceivedOrSent);
CoreService.AddOnMessageSentDelegate(OnMessageReceivedOrSent);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
@@ -61,16 +61,16 @@ namespace _04_BasicChat.Views
// You need to unregister delegate to allow the garbage collector to
// collect this instance when you navigate away.
CoreService.RemoveOnOnMessageReceivedDelegate(OnMessageReceiveOrSent);
CoreService.RemoveOnMessageSentDelegate(OnMessageReceiveOrSent);
CoreService.RemoveOnOnMessageReceivedDelegate(OnMessageReceivedOrSent);
CoreService.RemoveOnMessageSentDelegate(OnMessageReceivedOrSent);
base.OnNavigatedFrom(e);
}
/// <summary>
/// Method called too update the list every time a message is received or sent.
/// Method called to update the list every time a message is received or sent.
/// </summary>
private void OnMessageReceiveOrSent(Core core, ChatRoom chatRoom, ChatMessage message) => UpdateChatRooms();
private void OnMessageReceivedOrSent(Core core, ChatRoom chatRoom, ChatMessage message) => UpdateChatRooms();
public void UpdateChatRooms()
{
@@ -80,13 +80,13 @@ namespace _04_BasicChat.Views
// In the ChatRooms list attribute you can find every ChatRooms linked
// to your user. The list is ordered by ChatRoom last activity date
// (most recent first).
// You can see in Chats.xaml that we only use the properties
// You can see in ChatsPage.xaml that we only use the properties
// UnreadMessagesCount and PeerAdress to display our chat rooms.
// In further steps we will do more.
// In later steps we will do more.
foreach (ChatRoom chatRoom in CoreService.Core.ChatRooms)
{
// Here we use the HistorySize attribute to display only
// ChatRooms were at least one message was exchange.
// ChatRooms where at least one message was exchanged.
if (chatRoom.HistorySize > 0)
{
ChatRoomsLV.Items.Add(chatRoom);
@@ -110,31 +110,33 @@ namespace _04_BasicChat.Views
private async void NewChatRoom_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
string peerSipAddress = await InputTextDialogAsync("Enter peer sip address");
if (!String.IsNullOrWhiteSpace(peerSipAddress))
if (string.IsNullOrWhiteSpace(peerSipAddress))
{
// We create a new ChatRoom with the address the user gave us.
// See CoreService.CreateOrGetChatRoom(string sipAddress) for more info
ChatRoom newChatRoom = CoreService.CreateOrGetChatRoom(peerSipAddress);
return;
}
if (newChatRoom != null)
{
// If the ChatRoom creation succeed render/navigate to a ChatPage in the inner
// frame of the ChatsPage.
// See ChatPage.xaml.cs to understand how to get message history and how to send/receive
// and display new messages.
ChatRoomFrame.Navigate(typeof(ChatPage), newChatRoom);
}
else
{
ContentDialog noSettingsDialog = new ContentDialog
{
Title = "ChatRoom creation error",
Content = "An error occurred during ChatRoom creation, check sip address validity and try again.",
CloseButtonText = "OK"
};
// We create a new ChatRoom with the address the user gave us.
// See CoreService.CreateOrGetChatRoom(string sipAddress) for more info
ChatRoom newChatRoom = CoreService.CreateOrGetChatRoom(peerSipAddress);
await noSettingsDialog.ShowAsync();
}
if (newChatRoom != null)
{
// If the ChatRoom creation succeeded, render/navigate to a ChatPage in the inner
// frame of the ChatsPage.
// See ChatPage.xaml.cs to understand how to get message history and how to send/receive
// and display new messages.
ChatRoomFrame.Navigate(typeof(ChatPage), newChatRoom);
}
else
{
ContentDialog chatRoomCreationErrDialog = new ContentDialog
{
Title = "ChatRoom creation error",
Content = "An error occurred during ChatRoom creation, check sip address validity and try again.",
CloseButtonText = "OK"
};
await chatRoomCreationErrDialog.ShowAsync();
}
}

View File

@@ -28,7 +28,7 @@ using Windows.UI.Xaml.Navigation;
namespace _04_BasicChat.Views
{
/// <summary>
/// A really simple app for a first Login with LinphoneSDK x UWP
/// Introduced in step 02 IncomingCall
/// </summary>
public sealed partial class NavigationRoot : Page
{
@@ -44,26 +44,23 @@ namespace _04_BasicChat.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this.CoreService.AddOnOnMessageReceivedDelegate(OnMessageReveive);
this.CoreService.AddOnOnMessageReceivedDelegate(OnMessageReceived);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.CoreService.RemoveOnOnMessageReceivedDelegate(OnMessageReveive);
this.CoreService.RemoveOnOnMessageReceivedDelegate(OnMessageReceived);
base.OnNavigatedFrom(e);
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Only do an inital navigate the first time the page loads
// when we switch out of compactoverloadmode this will fire but we don't want to navigate because
// there is already a page loaded
if (!hasLoadedPreviously)
{
AppNavFrame.Navigate(typeof(CallsPage));
UpdateUnreadMessageCount();
hasLoadedPreviously = true;
NavigationService.CurrentNavigationRoot = this;
NavigationService.CurrentNavigationRoot = this; // NEW!
}
}
@@ -71,11 +68,12 @@ namespace _04_BasicChat.Views
{
switch (e.SourcePageType)
{
case Type c when e.SourcePageType == typeof(CallsPage):
case Type _ when e.SourcePageType == typeof(CallsPage):
((NavigationViewItem)navview.MenuItems[0]).IsSelected = true;
break;
case Type c when e.SourcePageType == typeof(ChatsPage):
// NEW!
case Type _ when e.SourcePageType == typeof(ChatsPage):
((NavigationViewItem)navview.MenuItems[1]).IsSelected = true;
break;
}
@@ -88,22 +86,20 @@ namespace _04_BasicChat.Views
ContentDialog noSettingsDialog = new ContentDialog
{
Title = "No settings",
Content = "There is no settings in this little app",
Content = "There are no settings in this little app",
CloseButtonText = "OK"
};
ContentDialogResult result = await noSettingsDialog.ShowAsync();
_ = await noSettingsDialog.ShowAsync();
return;
}
string invokedItemValue = args.InvokedItem as string;
if (invokedItemValue != null && invokedItemValue.Contains("Calls"))
if (args.InvokedItem is string invokedItemValue && invokedItemValue.Contains("Calls"))
{
AppNavFrame.Navigate(typeof(CallsPage));
_ = AppNavFrame.Navigate(typeof(CallsPage));
}
else
else // NEW!
{
AppNavFrame.Navigate(typeof(ChatsPage));
_ = AppNavFrame.Navigate(typeof(ChatsPage));
}
}
@@ -117,7 +113,7 @@ namespace _04_BasicChat.Views
ContentDialog signOutDialog = new ContentDialog
{
Title = "Sign out ?",
Content = "All your current calls and actions will be canceled, are you sure to continue ?",
Content = "All your current calls and actions will be canceled.",
PrimaryButtonText = "Sign out",
CloseButtonText = "Cancel"
};
@@ -133,20 +129,22 @@ namespace _04_BasicChat.Views
}
}
private void OnMessageReveive(Core core, ChatRoom chatRoom, ChatMessage message)
// NEW!
private void OnMessageReceived(Core core, ChatRoom chatRoom, ChatMessage message)
{
UpdateUnreadMessageCount();
}
// NEW!
public void UpdateUnreadMessageCount()
{
// The property UnreadChatMessageCountFromActiveLocals return the total
// number of unread messages in all the chat rooms off all connected accounts
// The property UnreadChatMessageCountFromActiveLocals gives the total
// number of unread messages in all the chat rooms of all the connected accounts
// on the device. In the tutorial we only allow one account at a time, so
// you get the global unread message count for your account.
if (CoreService.Core.UnreadChatMessageCountFromActiveLocals > 0)
{
NewMessageCount.Text = "" + CoreService.Core.UnreadChatMessageCountFromActiveLocals;
NewMessageCount.Text = CoreService.Core.UnreadChatMessageCountFromActiveLocals.ToString();
NewMessageCountBorder.Visibility = Visibility.Visible;
}
else

View File

@@ -192,10 +192,10 @@
<Version>2.1.13</Version>
</PackageReference>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -26,7 +26,7 @@
<TextBlock x:Name="FileName" Text="" FontWeight="Bold" />
<TextBlock x:Name="FileSize" Text="" />
<Button x:Name="Download" Content="Download" Click="Download_Click" Visibility="Collapsed" />
<Button x:Name="OpenFile" Content="Open file" Click="OpenFile_Click" Visibility="Collapsed" />
<Button x:Name="OpenFolder" Content="Open folder" Click="OpenFolder_Click" Visibility="Collapsed" />
</StackPanel>
<TextBlock x:Name="MessageState" />

View File

@@ -58,13 +58,13 @@ namespace _05_FileTransfer.Controls
private void OnMessageStateChanged(ChatMessage message, ChatMessageState state)
{
// We display the message state. It can be really useful for the user
// to know if the remote received the message (state = Delivered) or
// if he read it (state = Displayed)
// to know if the remote only received the message (state = Delivered) or
// if they read it (state = Displayed)
MessageState.Text = "The message state is : " + state;
switch (state)
{
// They're is multiple state during a file transfer (FileTransferInProgress,
// A file transfer can be in multiple states (FileTransferInProgress,
// FileTransferDone, FileTransferError). We update the layout if the file
// is done downloading to replace the "Download" button by an "Open file" button.
case ChatMessageState.FileTransferDone:
@@ -78,7 +78,7 @@ namespace _05_FileTransfer.Controls
MessageState.Text = "The message state is : " + ChatMessage.State;
// You can find the sending date of a ChatMessage in ChatMessage.Time.
// The time number respect the time_t type specification.
// The time number follows the time_t type specification. (Unix timestamp)
ReceiveDate.Text = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(ChatMessage.Time).ToLocalTime().ToString("HH:mm");
if (ChatMessage.IsOutgoing)
@@ -90,11 +90,11 @@ namespace _05_FileTransfer.Controls
this.HorizontalAlignment = HorizontalAlignment.Left;
}
// A ChatMessage hold a list of Content object, Contents.
// A ChatMessage holds a list of Content objects, Contents.
// But in a basic ChatRoom, using a basic backend, by default the multipart
// is disable. So in any received message there is only one Content in the list.
// is disabled. So in any received message there is only one Content in the list.
// You can enable multipart on a ChatRoom object with ChatRoom.AllowMultipart() but it
// can be risky. In fact if your remote doesn't support multipart and you send him
// can be risky. In fact if your remote doesn't support multipart and you send them
// a multipart message it could not work properly.
if (ChatMessage.Contents.Any(c => c.IsFile))
{
@@ -104,7 +104,7 @@ namespace _05_FileTransfer.Controls
// the Content object.
TextStack.Visibility = Visibility.Collapsed;
FileStack.Visibility = Visibility.Visible;
OpenFile.Visibility = Visibility.Visible;
OpenFolder.Visibility = Visibility.Visible;
Download.Visibility = Visibility.Collapsed;
// We can do this because we don't allowMultipart and can assume
@@ -126,7 +126,7 @@ namespace _05_FileTransfer.Controls
TextStack.Visibility = Visibility.Collapsed;
FileStack.Visibility = Visibility.Visible;
Download.Visibility = Visibility.Visible;
OpenFile.Visibility = Visibility.Collapsed;
OpenFolder.Visibility = Visibility.Collapsed;
Content content = ChatMessage.Contents.First((c) => c.IsFileTransfer);
@@ -173,16 +173,13 @@ namespace _05_FileTransfer.Controls
}
}
private async void OpenFile_Click(object sender, RoutedEventArgs e)
private async void OpenFolder_Click(object sender, RoutedEventArgs e)
{
// Just get the FilePath attribute from the Content object
string filePath = CurrentShownContent.FilePath;
// Only keep the folder part
string folderPath = filePath.Substring(0, filePath.LastIndexOf("\\"));
// And launch the Windows explorer
await Launcher.LaunchFolderAsync(await StorageFolder.GetFolderFromPathAsync(folderPath));
// Linphone can sometimes return paths with Unix-style forward slashes ('/').
// System.IO.Path.* methods are made to deal with such paths.
var folderPath = Path.GetDirectoryName(CurrentShownContent.FilePath);
var folder = await StorageFolder.GetFolderFromPathAsync(folderPath);
_ = await Launcher.LaunchFolderAsync(folder);
}
}
}

View File

@@ -3,15 +3,11 @@
Learn how to send files over SIP using Linphone SDK.
We added a button to send file to your peer, and we improved how messages are displayed to show
you more information about them and allow you to download files sent by the remote end.
Most of the new Linphone usage are in Controls/MessageDisplay.xaml(.cs) and ChatPage.xaml(.cs) but don't
We will add a button to send files to your peer, and improve the display of messages to
include more metadata and allow the user to download files sent by the remote end.
Most of the new Linphone uses are in Controls/MessageDisplay.xaml(.cs) and ChatPage.xaml(.cs) but don't
forget to set the attribute FileTransferServer on your Core ! (see Core creation in CoreService.cs)
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
- ANGLE.WindowsStore (for video rendering, version 2.1.13 recommended)
New/updated files :
@@ -19,7 +15,7 @@ New/updated files :
05_FileTransfer
└───Controls :
│ │ MessageDisplay.xaml(.cs) : A user control to display chat bubbles with more
│ │ information. Learn how to handle the different types of ChatMessage here.
│ │ information. Learn how to handle the different types of ChatMessage's here.
│ │
└───Service :
@@ -29,5 +25,5 @@ New/updated files :
└───Views :
│ │
│ │ ChatPage.xaml(.cs) : This is the frame displayed when you select a chat room.
│ │ You can now send file and the message display is improved (see MessageDisplay)
│ │ You can now send files and message display is improved (see MessageDisplay)
```

View File

@@ -75,10 +75,8 @@ namespace _05_FileTransfer.Service
videoActivationPolicy.AutomaticallyInitiate = false;
core.VideoActivationPolicy = videoActivationPolicy;
if (core.VideoSupported())
{
core.VideoCaptureEnabled = true;
}
core.VideoCaptureEnabled = core.VideoSupported();
core.UsePreviewWindow(true);
// You must set up your file transfer server if you want to transfer files.
@@ -186,17 +184,17 @@ namespace _05_FileTransfer.Service
Core.InviteAddress(address);
}
public bool MicEnabledSwitch()
public bool ToggleMic()
{
return Core.MicEnabled = !Core.MicEnabled;
}
public bool SpeakerMutedSwitch()
public bool ToggleSpeaker()
{
return Core.CurrentCall.SpeakerMuted = !Core.CurrentCall.SpeakerMuted;
}
public async Task<bool> CameraEnabledSwitchAsync()
public async Task<bool> ToggleCameraAsync()
{
await OpenCameraPopup();
@@ -235,21 +233,22 @@ namespace _05_FileTransfer.Service
// File Path is the only mandatory field to set.
content.FilePath = fileCopy.Path;
// You can set the type and subtype of your file, it help
// the server and receiver identifying the file (images can
// You can set the type and subtype of your file, it helps
// the server and receiver to identify the file (images can
// be directly displayed for example).
string[] splittedMimeType = fileCopy.ContentType.Split("/");
content.Type = splittedMimeType[0];
content.Subtype = splittedMimeType[1];
string[] splitMimeType = fileCopy.ContentType.Split("/");
content.Type = splitMimeType[0];
content.Subtype = splitMimeType[1];
// Set the file name for the receiver, by default the same name is taken.
// This line is useful only for the explanation.
// You can set the file name on the receiver's end. For the purposes of
// demonstration we set it to the original's name, but this is unnecessary
// since this is the default behaviour.
content.Name = fileCopy.Name;
return content;
}
private async Task OpenMicrophonePopup()
public async Task OpenMicrophonePopup()
{
AudioGraphSettings settings = new AudioGraphSettings(Windows.Media.Render.AudioRenderCategory.Media);
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
@@ -264,11 +263,18 @@ namespace _05_FileTransfer.Service
private async Task OpenCameraPopup()
{
MediaCapture mediaCapture = new Windows.Media.Capture.MediaCapture();
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
MediaCapture mediaCapture = new MediaCapture();
try
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
}
catch (Exception e) when (e.Message.StartsWith("No capture devices are available."))
{
// Ignored.
}
mediaCapture.Dispose();
}
}

View File

@@ -32,7 +32,7 @@
<TextBlock x:Name="CallText" Text="Your call state is : Idle" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10">
<Button x:Name="HangOut" Content="Hang out" Click="HangOutClick" IsEnabled="False" />
<Button x:Name="HangUp" Content="Hang up" Click="OnHangUpClicked" IsEnabled="False" />
<Button x:Name="Sound" Content="Switch off Sound" Click="SoundClick" IsEnabled="False" />
<Button x:Name="Camera" Content="Switch on Camera" Click="CameraClick" IsEnabled="False" />
<Button x:Name="Mic" Content="Mute" Click="MicClick" IsEnabled="False" />

View File

@@ -31,7 +31,7 @@ namespace _05_FileTransfer.Views
private VideoService VideoService { get; } = VideoService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@@ -63,14 +63,14 @@ namespace _05_FileTransfer.Views
CoreService.Call(UriToCall.Text);
}
private void HangOutClick(object sender, RoutedEventArgs e)
private void OnHangUpClicked(object sender, RoutedEventArgs e)
{
CoreService.Core.TerminateAllCalls();
}
private void SoundClick(object sender, RoutedEventArgs e)
{
if (CoreService.SpeakerMutedSwitch())
if (CoreService.ToggleSpeaker())
{
Sound.Content = "Switch on Sound";
}
@@ -82,14 +82,14 @@ namespace _05_FileTransfer.Views
private async void CameraClick(object sender, RoutedEventArgs e)
{
await CoreService.CameraEnabledSwitchAsync();
await CoreService.ToggleCameraAsync();
Camera.Content = "Waiting for accept ...";
Camera.IsEnabled = false;
}
private void MicClick(object sender, RoutedEventArgs e)
{
if (CoreService.MicEnabledSwitch())
if (CoreService.ToggleMic())
{
Mic.Content = "Mute";
}
@@ -99,18 +99,19 @@ namespace _05_FileTransfer.Views
}
}
private void AnswerClick(object sender, RoutedEventArgs e)
private async void AnswerClick(object sender, RoutedEventArgs e)
{
IncommingCall.Accept();
IncommingCall = null;
await CoreService.OpenMicrophonePopup();
IncomingCall.Accept();
IncomingCall = null;
}
private void DeclineClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
IncommingCall.Decline(Reason.Declined);
IncommingCall = null;
IncomingCall.Decline(Reason.Declined);
IncomingCall = null;
}
}
@@ -121,7 +122,7 @@ namespace _05_FileTransfer.Views
{
case CallState.IncomingReceived:
IncommingCall = call;
IncomingCall = call;
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + call.RemoteAddress.AsString();
break;
@@ -130,7 +131,7 @@ namespace _05_FileTransfer.Views
case CallState.OutgoingProgress:
case CallState.OutgoingRinging:
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
break;
case CallState.StreamsRunning:
@@ -151,7 +152,7 @@ namespace _05_FileTransfer.Views
case CallState.End:
case CallState.Released:
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
VideoService.StopVideoStream();
break;
@@ -178,7 +179,7 @@ namespace _05_FileTransfer.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = true;
HangOut.IsEnabled = false;
HangUp.IsEnabled = false;
Sound.IsEnabled = false;
Camera.IsEnabled = false;
Mic.IsEnabled = false;
@@ -192,7 +193,7 @@ namespace _05_FileTransfer.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = false;
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
Sound.IsEnabled = true;
Camera.IsEnabled = true;
Mic.IsEnabled = true;

View File

@@ -44,7 +44,7 @@ namespace _05_FileTransfer.Views
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ChatRoom = ((ChatRoom)e.Parameter);
ChatRoom = (ChatRoom)e.Parameter;
ChatHeaderText.Text += ChatRoom.PeerAddress.Username;
ChatRoom.Listener.OnMessageReceived += OnMessageReceived;
foreach (ChatMessage chatMessage in ChatRoom.GetHistory(0))
@@ -76,7 +76,7 @@ namespace _05_FileTransfer.Views
private void AddMessage(ChatMessage chatMessage)
{
// Instead of simply display a TextBlock we now create a
// Instead of simply displaying a TextBlock we now create a
// MessageDisplay object to show more informations about the message.
// See Controls/MessageDisplay.xaml(.cs)
MessageDisplay messageDisplay = new MessageDisplay(chatMessage);
@@ -110,9 +110,11 @@ namespace _05_FileTransfer.Views
{
// Basic Windows code to let the user select a file and gain
// read access to a StorageFile object.
FileOpenPicker picker = new FileOpenPicker();
picker.ViewMode = PickerViewMode.List;
picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
FileOpenPicker picker = new FileOpenPicker
{
ViewMode = PickerViewMode.List,
SuggestedStartLocation = PickerLocationId.DocumentsLibrary
};
picker.FileTypeFilter.Add("*");
StorageFile file = await picker.PickSingleFileAsync();

View File

@@ -28,7 +28,8 @@ using Windows.UI.Xaml.Navigation;
namespace _05_FileTransfer.Views
{
/// <summary>
/// A really simple app for a first Login with LinphoneSDK x UWP
/// Introduced in step 02 IncomingCall
/// Changed in step 04 BasicChat
/// </summary>
public sealed partial class NavigationRoot : Page
{
@@ -55,9 +56,6 @@ namespace _05_FileTransfer.Views
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// Only do an inital navigate the first time the page loads
// when we switch out of compactoverloadmode this will fire but we don't want to navigate because
// there is already a page loaded
if (!hasLoadedPreviously)
{
AppNavFrame.Navigate(typeof(CallsPage));
@@ -71,11 +69,11 @@ namespace _05_FileTransfer.Views
{
switch (e.SourcePageType)
{
case Type c when e.SourcePageType == typeof(CallsPage):
case Type _ when e.SourcePageType == typeof(CallsPage):
((NavigationViewItem)navview.MenuItems[0]).IsSelected = true;
break;
case Type c when e.SourcePageType == typeof(ChatsPage):
case Type _ when e.SourcePageType == typeof(ChatsPage):
((NavigationViewItem)navview.MenuItems[1]).IsSelected = true;
break;
}
@@ -88,22 +86,21 @@ namespace _05_FileTransfer.Views
ContentDialog noSettingsDialog = new ContentDialog
{
Title = "No settings",
Content = "There is no settings in this little app",
Content = "There are no settings in this little app",
CloseButtonText = "OK"
};
ContentDialogResult result = await noSettingsDialog.ShowAsync();
_ = await noSettingsDialog.ShowAsync();
return;
}
string invokedItemValue = args.InvokedItem as string;
if (invokedItemValue != null && invokedItemValue.Contains("Calls"))
{
AppNavFrame.Navigate(typeof(CallsPage));
_ = AppNavFrame.Navigate(typeof(CallsPage));
}
else
{
AppNavFrame.Navigate(typeof(ChatsPage));
_ = AppNavFrame.Navigate(typeof(ChatsPage));
}
}
@@ -117,7 +114,7 @@ namespace _05_FileTransfer.Views
ContentDialog signOutDialog = new ContentDialog
{
Title = "Sign out ?",
Content = "All your current calls and actions will be canceled, are you sure to continue ?",
Content = "All your current calls and actions will be canceled.",
PrimaryButtonText = "Sign out",
CloseButtonText = "Cancel"
};

View File

@@ -225,10 +225,10 @@
<Version>2.1.13</Version>
</PackageReference>
<PackageReference Include="LinphoneSDK">
<Version>5.1.0-alpha.56</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.11</Version>
<Version>6.2.13</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -14,7 +14,7 @@
<TextBlock x:Name="FileName" Text="" FontWeight="Bold" />
<TextBlock x:Name="FileSize" Text="" />
<Button x:Name="Download" Content="Download" Click="Download_Click" Visibility="Collapsed" />
<Button x:Name="OpenFile" Content="Open file" Click="OpenFile_Click" Visibility="Collapsed" />
<Button x:Name="OpenFolder" Content="Open folder" Click="OpenFolder_Click" Visibility="Collapsed" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -43,7 +43,7 @@ namespace _06_GroupChat.Controls
private void UpdateLayoutFromContent()
{
// We kept the code from the old MessageDisplay class. Working
// with a single object instead of a List of content make it
// with a single object instead of a List of content makes it
// cleaner.
if (DisplayedContent.IsFile || DisplayedContent.IsFileTransfer)
{
@@ -53,15 +53,15 @@ namespace _06_GroupChat.Controls
FileName.Text = DisplayedContent.Name;
FileSize.Text = DisplayedContent.FileSize + " bits";
if (DisplayedContent.IsFile || DisplayedContent.IsFileTransfer && ChatMessage.IsOutgoing)
if (DisplayedContent.IsFile || (DisplayedContent.IsFileTransfer && ChatMessage.IsOutgoing))
{
OpenFile.Visibility = Visibility.Visible;
OpenFolder.Visibility = Visibility.Visible;
Download.Visibility = Visibility.Collapsed;
}
else
{
Download.Visibility = Visibility.Visible;
OpenFile.Visibility = Visibility.Collapsed;
OpenFolder.Visibility = Visibility.Collapsed;
}
}
else if (DisplayedContent.IsText)
@@ -85,12 +85,11 @@ namespace _06_GroupChat.Controls
ChatMessage.DownloadContent(DisplayedContent);
}
private async void OpenFile_Click(object sender, RoutedEventArgs e)
private async void OpenFolder_Click(object sender, RoutedEventArgs e)
{
string filePath = DisplayedContent.FilePath;
string folderPath = filePath.Substring(0, filePath.LastIndexOf("\\"));
await Launcher.LaunchFolderAsync(await StorageFolder.GetFolderFromPathAsync(folderPath));
var folderPath = Path.GetDirectoryName(DisplayedContent.FilePath);
var folder = await StorageFolder.GetFolderFromPathAsync(folderPath);
_ = await Launcher.LaunchFolderAsync(folder);
}
}
}

View File

@@ -28,9 +28,9 @@ namespace _06_GroupChat.Controls
{
// An EventDisplay is always linked to an EventLog object and is displayed at the center
// of the message list.
this.InitializeComponent();
InitializeComponent();
// After we simply create the text we want to display based on the type on event
// Then we simply create the text we want to display based on the type of event
// and from information we get from the EventLog object.
switch (eventLog.Type)
{
@@ -44,30 +44,30 @@ namespace _06_GroupChat.Controls
EventText.Text = $"The conference {eventLog.Subject} is terminated";
break;
case EventLogType.ConferenceCallStart:
EventText.Text = "Call start";
case EventLogType.ConferenceCallStarted:
EventText.Text = "Call started";
break;
case EventLogType.ConferenceCallEnd:
EventText.Text = "Call end";
case EventLogType.ConferenceCallEnded:
EventText.Text = "Call ended";
break;
case EventLogType.ConferenceParticipantAdded:
// Or you can access a ParticipantAddress attribute when the type of
// event is linked to a participant.
EventText.Text = $"{eventLog.ParticipantAddress.Username} is added";
EventText.Text = $"{eventLog.ParticipantAddress.Username} joined";
break;
case EventLogType.ConferenceParticipantRemoved:
EventText.Text = $"{eventLog.ParticipantAddress.Username} is removed";
EventText.Text = $"{eventLog.ParticipantAddress.Username} left";
break;
case EventLogType.ConferenceParticipantSetAdmin:
EventText.Text = $"{eventLog.ParticipantAddress.Username} is now admin";
EventText.Text = $"{eventLog.ParticipantAddress.Username} is now an admin";
break;
case EventLogType.ConferenceParticipantUnsetAdmin:
EventText.Text = $"{eventLog.ParticipantAddress.Username} admin status removed";
EventText.Text = $"{eventLog.ParticipantAddress.Username} is no longer an admin";
break;
case EventLogType.ConferenceSubjectChanged:
@@ -76,4 +76,4 @@ namespace _06_GroupChat.Controls
}
}
}
}
}

View File

@@ -88,10 +88,10 @@ namespace _06_GroupChat.Controls
ParticipantsLV.Items.Clear();
// You can find the participant list in the ChatRoom.Participants attribute.
// You can note that the participant list doesn't contain yourself.
// Note that the participant list doesn't contain yourself.
foreach (Participant participant in ChatRoom.Participants)
{
if (participant.Address != null && !String.IsNullOrWhiteSpace(participant.Address.Username))
if (participant.Address != null && !string.IsNullOrWhiteSpace(participant.Address.Username))
{
ParticipantsLV.Items.Add(participant);
}
@@ -106,10 +106,10 @@ namespace _06_GroupChat.Controls
Participant participantToRemove = (Participant)((Button)sender).Tag;
// To remove a participant simply use the RemoveParticipant(Participant participant) method
// on a ChatRoom object. If you are admin and the participant is present in the ChatRoom he
// on a ChatRoom object. If you are admin and the participant is present in the ChatRoom they
// will be removed.
// The method RemoveParticipants(IEnumerable<Participant> participants) also exist if you want to
// remove multiple participant at once.
// The method RemoveParticipants(IEnumerable<Participant> participants) also exists if you want to
// remove multiple participants at once.
ChatRoom.RemoveParticipant(participantToRemove);
}
@@ -118,24 +118,24 @@ namespace _06_GroupChat.Controls
/// </summary>
private async void AddParticipant_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
string peerSipAddress = await Utils.InputTextDialogAsync("Enter peer sip address");
string peerSipAddress = await Utils.InputTextDialogAsync("Enter peer SIP address");
Address address = CoreService.Core.InterpretUrl(peerSipAddress);
if (address != null)
{
// To add a participant simply call the method AddParticipant(Address addr).
// If you are admin and the participant have a device that can handle
// group chat connected to the conference server he will be added.
// You can use AddParticipants(IEnumerable<Address> addresses) to add multiple
// If you are admin and the participant has a device that can handle
// group chats connected to the conference server, they will be added.
// You can also use AddParticipants(IEnumerable<Address> addresses) to add multiple
// participants at once.
// Here we use Core.InterpretUrl to transform a string sip address to a valid
// Linphone.Address object as we done multiple times before.
// Linphone.Address object as we have done multiple times before.
ChatRoom.AddParticipant(address);
}
else
{
ContentDialog badAddressDialog = new ContentDialog
{
Title = "Adding participant failed",
Title = "Failed to add participant",
Content = "An error occurred during address interpretation, check sip address validity and try again.",
CloseButtonText = "OK"
};
@@ -152,7 +152,7 @@ namespace _06_GroupChat.Controls
Participant participantToUpgrade = (Participant)((Button)sender).Tag;
// Use the SetParticipantAdminStatus(Participant participant, bool isAdmin) to change
// the admin of a participant, you must be admin yourself if you want this action to work.
// the admin status of a participant, you must be admin yourself if you want this action to work.
ChatRoom.SetParticipantAdminStatus(participantToUpgrade, !participantToUpgrade.IsAdmin);
}

View File

@@ -68,11 +68,11 @@ namespace _06_GroupChat.Controls
if (ChatMessage.IsOutgoing)
{
this.HorizontalAlignment = HorizontalAlignment.Right;
HorizontalAlignment = HorizontalAlignment.Right;
}
else
{
this.HorizontalAlignment = HorizontalAlignment.Left;
HorizontalAlignment = HorizontalAlignment.Left;
}
}
@@ -82,9 +82,9 @@ namespace _06_GroupChat.Controls
// We iterate over the Contents list to display all the contents
// in a multipart message.
// This code is common for Basic and Flexisip ChatRoom so even if
// another SIP client don't respect the basic chat room rules and
// and send multipart we can display it.
// This code is the same for Basic and Flexisip ChatRoom so even if
// another SIP client doesn't follow the basic chat room rules and
// and sends multipart content we can display it.
foreach (Content content in ChatMessage.Contents)
{
AddContent(content);
@@ -93,7 +93,7 @@ namespace _06_GroupChat.Controls
private void AddContent(Content content)
{
// A Content object can himself be multipart
// A Content object can itself be multipart
if (content.IsMultipart)
{
// So we make this method recursive
@@ -104,7 +104,7 @@ namespace _06_GroupChat.Controls
return;
}
// And we create a content display for each content. You can watch the code
// And we create a content display for each content. You can look at the code
// in content ContentDisplay.xaml(.cs).
ContentDisplay contentDisplay = new ContentDisplay(content, ChatMessage);
ContentsStack.Children.Add(contentDisplay);

View File

@@ -1,26 +1,21 @@
Linphone X UWP tutorial 06_group_chat
========================================
In this step we are going to approach to new concepts: group chat and multipart message. To enable
In this step we will tackle two new concepts: group chats and multipart messages. To enable
those new features we are going to use Flexisip as a backend (see group ChatRoom creation). Flexisip
is a complete and modular SIP server suite. If you need more informations about Flexisip you can read
this [Flexisip presentation](http://linphone.org/technical-corner/flexisip) or
[Contact us](http://linphone.org/contact).
We created a new page so you can prepare your participant list before creating a group chat, see
CreateGroupChatRoom.xaml(.cs) and CoreService.cs to learn how to create group chat.
We will create a new page so you can prepare your participant list before creating a group chat, see
CreateGroupChatRoom.xaml(.cs) and CoreService.cs to learn how to create group chats.
We also updated the ChatPage.xaml(.cs) and the MessageDisplay.xaml(cs) so you can send and display multipart
messages correctly. Multipart message are allowed by default in flexisip, you can try it right now in group chat room.
In group ChatRoom some events that can occur (subject change, admin status modification...) are also displayed, see EventDisplay.xaml(.cs).
And last we change the way we display the ChatRoom in the list (ChatsPage.xml) see ChatRoomToStringConverter.cs.
We will update the ChatPage.xaml(.cs) and the MessageDisplay.xaml(cs) so you can send and display multipart
messages correctly. Multipart messages are allowed by default in flexisip, you can try it right now in a group chat room.
In a group ChatRoom some events that can occur (subject change, admin status modification...) will also be displayed. See EventDisplay.xaml(.cs).
And lastly we will change the way we display the ChatRoom in the list (ChatsPage.xml). See ChatRoomToStringConverter.cs.
Don't forget to install those NuGet packages :
- LinphoneSDK (can be found here : https://www.linphone.org/snapshots/windows/sdk/)
- Microsoft.NETCore.UniversalWindowsPlatform (version 6.2.12 recommended)
- ANGLE.WindowsStore (for video rendering, version 2.1.13 recommended)
New/updated files :
```
@@ -43,13 +38,13 @@ New/updated files :
└───Service :
│ │ CoreService.cs : A singleton service which contains the Linphone.Core.
│ │ Watch the LogIn method to see how to setup a conference factory.
│ │ Take a look at the LogIn method to see how to setup a conference factory.
└───Shared :
│ │ ChatRoomToStringConverter.cs : a class that implement IValueConverter to display the
│ │ the chat room name according to its type.
│ │ ChatRoomToStringConverter.cs : a class that implements IValueConverter to display
│ │ the chat room name depending on its type.
│ │
│ │ Utils.cs : Utility class to regroup static methods used in different other classes.
│ │ Utils.cs : Utility class to gather static methods used in different other classes.
└───Views :
│ │

View File

@@ -63,14 +63,14 @@ namespace _06_GroupChat.Service
factory.ImageResourcesDir = Path.Combine(assetsPath, "images");
factory.MspluginsDir = ".";
// In a Flexisip ChatRoom you are identified by your authentication info and your device (you can have multiple device
// connected to your account, some can accept group chat and some cannot). To identify your different devices Linphone
// use UUID generated when your start your app for the first time on your device. This UUID is stored in a configuration
// file, this is why we specify a file for this configuration file now, if you don't every time you will start your app
// In a Flexisip ChatRoom you are identified by your authentication info and your device (you can have multiple devices
// connected to your account, some may accept group chat and others not). To identify your different devices, Linphone
// uses a UUID generated when you start your app for the first time on the device. This UUID is stored in a configuration
// file, this is why we specify a path for this configuration file now. If you don't, every time you start your app
// it will be identified as a new device.
// A second effect of this you will soon notify is that your authentication informations are also stored in this file
// and are loaded at core startup. So if you don't use the sign out button and simply close the app the next time you won't
// have to login, it will be automatic.
// A side-effect to this you will soon notice is that your authentication information is also stored in this file
// and is loaded at core startup. So if you don't use the sign out button and simply close the app, it will log you
// back in the next time it starts.
core = factory.CreateCore(Path.Combine(ApplicationData.Current.LocalFolder.Path, "configuration"), "", IntPtr.Zero);
core.AudioPort = 7666;
@@ -84,10 +84,8 @@ namespace _06_GroupChat.Service
videoActivationPolicy.AutomaticallyInitiate = false;
core.VideoActivationPolicy = videoActivationPolicy;
if (core.VideoSupported())
{
core.VideoCaptureEnabled = true;
}
core.VideoCaptureEnabled = core.VideoSupported();
core.UsePreviewWindow(true);
core.FileTransferServer = "https://www.linphone.org:444/lft.php";
@@ -209,17 +207,17 @@ namespace _06_GroupChat.Service
Core.InviteAddress(address);
}
public bool MicEnabledSwitch()
public bool ToggleMic()
{
return Core.MicEnabled = !Core.MicEnabled;
}
public bool SpeakerMutedSwitch()
public bool ToggleSpeaker()
{
return Core.CurrentCall.SpeakerMuted = !Core.CurrentCall.SpeakerMuted;
}
public async Task<bool> CameraEnabledSwitchAsync()
public async Task<bool> ToggleCameraAsync()
{
await OpenCameraPopup();
@@ -266,7 +264,7 @@ namespace _06_GroupChat.Service
chatRoomParams.RttEnabled = false;
// Now you can create your group chat room. the participants list must be not empty.
// With the different information you send the conference factory will try to create your ChatRoom.
// The conference factory will attempt to create a ChatRoom from the configuration you pass it.
// See ChatPage.OnNavigatedTo to see how to know when your ChatRoom is ready.
return Core.CreateChatRoom(chatRoomParams, localAdress, participants);
}
@@ -278,23 +276,20 @@ namespace _06_GroupChat.Service
Content content = Core.CreateContent();
content.FilePath = fileCopy.Path;
string[] splittedMimeType = fileCopy.ContentType.Split("/");
content.Type = splittedMimeType[0];
content.Subtype = splittedMimeType[1];
// Set the file name for the receiver
content.Name = fileCopy.Name;
string[] splitMimeType = fileCopy.ContentType.Split("/");
content.Type = splitMimeType[0];
content.Subtype = splitMimeType[1];
return content;
}
private async Task OpenMicrophonePopup()
public async Task OpenMicrophonePopup()
{
AudioGraphSettings settings = new AudioGraphSettings(Windows.Media.Render.AudioRenderCategory.Media);
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
AudioGraph audioGraph = result.Graph;
CreateAudioDeviceInputNodeResult resultNode = await audioGraph.CreateDeviceInputNodeAsync(Windows.Media.Capture.MediaCategory.Media);
CreateAudioDeviceInputNodeResult resultNode = await audioGraph.CreateDeviceInputNodeAsync(MediaCategory.Media);
AudioDeviceInputNode deviceInputNode = resultNode.DeviceInputNode;
deviceInputNode.Dispose();
@@ -303,11 +298,18 @@ namespace _06_GroupChat.Service
private async Task OpenCameraPopup()
{
MediaCapture mediaCapture = new Windows.Media.Capture.MediaCapture();
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
MediaCapture mediaCapture = new MediaCapture();
try
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
await mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Video
});
}
catch (Exception e) when (e.Message.StartsWith("No capture devices are available."))
{
// Ignored.
}
mediaCapture.Dispose();
}
}

View File

@@ -20,7 +20,7 @@ namespace _06_GroupChat.Shared
else if (chatRoom.HasCapability((int)ChatRoomCapabilities.OneToOne))
{
// If the ChatRoom is a OneToOne conference (we will speak more about those in further steps)
nameInList = chatRoom.Participants.FirstOrDefault() == null ? "" : chatRoom.Participants.First().Address.Username;
nameInList = chatRoom.Participants.FirstOrDefault()?.Address.Username;
}
else if (chatRoom.HasCapability((int)ChatRoomCapabilities.Conference))
{
@@ -28,7 +28,7 @@ namespace _06_GroupChat.Shared
nameInList = chatRoom.Subject;
}
if (String.IsNullOrEmpty(nameInList))
if (string.IsNullOrEmpty(nameInList))
{
nameInList = "Incoherent ChatRoom values";
}

View File

@@ -32,7 +32,7 @@
<TextBlock x:Name="CallText" Text="Your call state is : Idle" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10">
<Button x:Name="HangOut" Content="Hang out" Click="HangOutClick" IsEnabled="False" />
<Button x:Name="HangUp" Content="Hang up" Click="OnHangUpClicked" IsEnabled="False" />
<Button x:Name="Sound" Content="Switch off Sound" Click="SoundClick" IsEnabled="False" />
<Button x:Name="Camera" Content="Switch on Camera" Click="CameraClick" IsEnabled="False" />
<Button x:Name="Mic" Content="Mute" Click="MicClick" IsEnabled="False" />

View File

@@ -31,7 +31,7 @@ namespace _06_GroupChat.Views
private VideoService VideoService { get; } = VideoService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@@ -63,14 +63,14 @@ namespace _06_GroupChat.Views
CoreService.Call(UriToCall.Text);
}
private void HangOutClick(object sender, RoutedEventArgs e)
private void OnHangUpClicked(object sender, RoutedEventArgs e)
{
CoreService.Core.TerminateAllCalls();
}
private void SoundClick(object sender, RoutedEventArgs e)
{
if (CoreService.SpeakerMutedSwitch())
if (CoreService.ToggleSpeaker())
{
Sound.Content = "Switch on Sound";
}
@@ -82,14 +82,14 @@ namespace _06_GroupChat.Views
private async void CameraClick(object sender, RoutedEventArgs e)
{
await CoreService.CameraEnabledSwitchAsync();
await CoreService.ToggleCameraAsync();
Camera.Content = "Waiting for accept ...";
Camera.IsEnabled = false;
}
private void MicClick(object sender, RoutedEventArgs e)
{
if (CoreService.MicEnabledSwitch())
if (CoreService.ToggleMic())
{
Mic.Content = "Mute";
}
@@ -99,18 +99,19 @@ namespace _06_GroupChat.Views
}
}
private void AnswerClick(object sender, RoutedEventArgs e)
private async void AnswerClick(object sender, RoutedEventArgs e)
{
IncommingCall.Accept();
IncommingCall = null;
await CoreService.OpenMicrophonePopup();
IncomingCall.Accept();
IncomingCall = null;
}
private void DeclineClick(object sender, RoutedEventArgs e)
{
if (IncommingCall != null)
if (IncomingCall != null)
{
IncommingCall.Decline(Reason.Declined);
IncommingCall = null;
IncomingCall.Decline(Reason.Declined);
IncomingCall = null;
}
}
@@ -121,7 +122,7 @@ namespace _06_GroupChat.Views
{
case CallState.IncomingReceived:
IncommingCall = call;
IncomingCall = call;
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + call.RemoteAddress.AsString();
break;
@@ -130,7 +131,7 @@ namespace _06_GroupChat.Views
case CallState.OutgoingProgress:
case CallState.OutgoingRinging:
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
break;
case CallState.StreamsRunning:
@@ -151,7 +152,7 @@ namespace _06_GroupChat.Views
case CallState.End:
case CallState.Released:
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
VideoService.StopVideoStream();
break;
@@ -178,7 +179,7 @@ namespace _06_GroupChat.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = true;
HangOut.IsEnabled = false;
HangUp.IsEnabled = false;
Sound.IsEnabled = false;
Camera.IsEnabled = false;
Mic.IsEnabled = false;
@@ -192,7 +193,7 @@ namespace _06_GroupChat.Views
{
IncomingCallStackPanel.Visibility = Visibility.Collapsed;
CallButton.IsEnabled = false;
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
Sound.IsEnabled = true;
Camera.IsEnabled = true;
Mic.IsEnabled = true;

Some files were not shown because too many files have changed in this diff Show More