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)
This commit is contained in:
Thibault Lemaire 2021-12-23 16:49:01 +01:00
parent 083ddb165b
commit aca479888a
64 changed files with 600 additions and 583 deletions

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)
{
@ -45,29 +45,29 @@ namespace _06_GroupChat.Controls
break;
case EventLogType.ConferenceCallStart:
EventText.Text = "Call start";
EventText.Text = "Call started";
break;
case EventLogType.ConferenceCallEnd:
EventText.Text = "Call end";
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:

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;

View File

@ -48,7 +48,7 @@ namespace _06_GroupChat.Views
ChatRoom.Listener.OnMessageReceived += OnMessageReceived;
// Here we register to almost all the different events that can appear on a conference.
// Here we register to almost all the different events that can happen on a conference.
// We use the same method AddEvent to handle all of them.
ChatRoom.Listener.OnConferenceLeft += AddEvent;
ChatRoom.Listener.OnConferenceJoined += AddEvent;
@ -59,7 +59,7 @@ namespace _06_GroupChat.Views
ChatRoom.Listener.OnParticipantAdded += AddEvent;
// If the peer address is null it means we are not in a basic
// ChatRoom, and that we have to wait the answer from the
// ChatRoom, and that we have to wait for the answer from the
// conference server.
if (ChatRoom.PeerAddress != null)
{
@ -104,7 +104,7 @@ namespace _06_GroupChat.Views
// means everything for GetHistoryEvents.
foreach (EventLog eventLog in ChatRoom.GetHistoryEvents(0))
{
// If the event is a message we do like before
// If the event is a message we do as before
if (EventLogType.ConferenceChatMessage.Equals(eventLog.Type))
{
AddMessage(eventLog.ChatMessage);
@ -132,7 +132,7 @@ namespace _06_GroupChat.Views
GroupChatDisplayBorder.Visibility = Visibility.Visible;
}
// We don't allow the user to send multipart message in basic ChatRoom
// We don't allow the user to send multipart messages in a basic ChatRoom
if (ChatRoom.HasCapability((int)ChatRoomCapabilities.Basic))
{
SendMultipartButton.Visibility = Visibility.Collapsed;
@ -162,7 +162,7 @@ namespace _06_GroupChat.Views
// Here we simply create an event display control ...
EventDisplay eventDisplay = new EventDisplay(eventLog);
// ... and adding it to the message list.
// ... and add it to the message list.
MessagesList.Children.Add(eventDisplay);
// See EventDisplay.xaml(.cs) to see how we handle events.

View File

@ -45,8 +45,8 @@ namespace _06_GroupChat.Views
CoreService.AddOnOnMessageReceivedDelegate(OnMessageReceiveOrSent);
CoreService.AddOnMessageSentDelegate(OnMessageReceiveOrSent);
// We registered to this callback so we can update the list with the new
// name when a chat room subject change.
// We registered this callback to update the list with the new
// name when a chat room subject is changed.
CoreService.AddOnChatRoomSubjectChangedDelegate(AddOnChatRoomSubjectChanged);
}
@ -61,7 +61,7 @@ namespace _06_GroupChat.Views
private void OnMessageReceiveOrSent(Core core, ChatRoom chatRoom, ChatMessage message) => UpdateChatRooms();
private void AddOnChatRoomSubjectChanged(Core core, ChatRoom chatRoom) => UpdateChatRooms();
private void AddOnChatRoomSubjectChanged(Core core, ChatRoom chatRoom) => UpdateChatRooms(); // NEW!
public void UpdateChatRooms()
{
@ -90,7 +90,7 @@ namespace _06_GroupChat.Views
private async void NewChatRoom_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
string peerSipAddress = await Utils.InputTextDialogAsync("Enter peer sip address");
if (!String.IsNullOrWhiteSpace(peerSipAddress))
if (!string.IsNullOrWhiteSpace(peerSipAddress))
{
ChatRoom newChatRoom = CoreService.CreateOrGetChatRoom(peerSipAddress);
if (newChatRoom != null)
@ -113,7 +113,7 @@ namespace _06_GroupChat.Views
private void NewGroupChatRoom_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
// To create a new group chat room you need a list of participant and a subject.
// To create a new group chat room you need a list of participants and a subject.
// See CreateGroupChatRoom.xaml(.cs) to see how to create a group chat room.
ChatRoomFrame.Navigate(typeof(CreateGroupChatRoom));
}

View File

@ -34,41 +34,41 @@ namespace _06_GroupChat.Views
public ObservableCollection<Address> DisplayedAddresses
{
get { return this.addresses; }
get { return addresses; }
}
public CreateGroupChatRoom()
{
this.InitializeComponent();
InitializeComponent();
}
private async void Create_Click(object sender, RoutedEventArgs e)
{
// This page goal is to allow the allow the user to choose the subject of
// his group chat room and to prepare the list of participant.
// With this two things we can create a group chat room, see CoreService.CreateGroupChatRoom
// The purpose of this page is to allow the user to choose the subject of
// their group chat room and to prepare the list of participants.
// With these two things we can create a group chat room, see CoreService.CreateGroupChatRoom
// to learn how to create it !
ChatRoom newChatRoom = CoreService.CreateGroupChatRoom(DisplayedAddresses, Subject.Text);
if (newChatRoom != null)
{
this.Frame.Navigate(typeof(ChatPage), newChatRoom);
Frame.Navigate(typeof(ChatPage), newChatRoom);
}
else
{
ContentDialog noSettingsDialog = new ContentDialog
ContentDialog errorDialog = new ContentDialog
{
Title = "ChatRoom creation error",
Content = "An error occurred during group ChatRoom creation, check sip addresses validity and try again.",
CloseButtonText = "OK"
};
await noSettingsDialog.ShowAsync();
await errorDialog.ShowAsync();
}
}
private void AddAddress_Click(object sender, RoutedEventArgs e)
{
if (!String.IsNullOrWhiteSpace(Address.Text))
if (!string.IsNullOrWhiteSpace(Address.Text))
{
DisplayedAddresses.Add(CoreService.Core.InterpretUrl(Address.Text));
AddressesLV.ItemsSource = DisplayedAddresses;

View File

@ -28,7 +28,8 @@ using Windows.UI.Xaml.Navigation;
namespace _06_GroupChat.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
{
@ -60,7 +61,7 @@ namespace _06_GroupChat.Views
// there is already a page loaded
if (!hasLoadedPreviously)
{
AppNavFrame.Navigate(typeof(CallsPage));
_ = AppNavFrame.Navigate(typeof(CallsPage));
UpdateUnreadMessageCount();
hasLoadedPreviously = true;
NavigationService.CurrentNavigationRoot = this;
@ -71,11 +72,11 @@ namespace _06_GroupChat.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;
}
@ -91,19 +92,17 @@ namespace _06_GroupChat.Views
Content = "There is 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
{
AppNavFrame.Navigate(typeof(ChatsPage));
_ = AppNavFrame.Navigate(typeof(ChatsPage));
}
}

View File

@ -222,10 +222,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

@ -121,11 +121,11 @@ namespace _07_AdvancedChat.Controls
private void OnEphemeralMessageTimerStarted(ChatMessage message)
{
// Here we create a basic timer with the windows UI library, you can
// just note that we can found the ephemeral lifetime of this message
// as a read only attribute (ChatMessage.EphemeralLifetime).
// Here we create a basic timer with the windows UI library.
// Simply note that the ephemeral lifetime is available
// as a read only attribute of a ChatMessage.
Basetime = ChatMessage.EphemeralLifetime;
EphemeralLifetime.Text = $"{Basetime.ToString()} remaining before deletion";
EphemeralLifetime.Text = $"{Basetime} remaining before deletion";
// See Timer_Tick
Timer.Start();
@ -134,9 +134,14 @@ namespace _07_AdvancedChat.Controls
private void OnEphemeralMessageDeleted(ChatMessage message)
{
// When this callback is triggered we erase the message from the view.
// Be careful, if you communicate with different clients some can choose
// to keep the message displayed even if it was an ephemeral one !
this.Content = new TextBlock
// Note on security:
// As you can see ephemeral messages are a client-side only feature.
// This means that a malicious client program can easily keep a "deleted" ephemeral message.
// You shouldn't rely on ephemeral messages if you cannot make sure all participants of
// a conversation use compliant applications.
Content = new TextBlock
{
Text = "deleted ephemeral message"
};

View File

@ -4,7 +4,7 @@
You reached the last step of this tutorial ! Well done ! In this step we will show you
how to create group chat, enable encryption and send ephemeral messages.
Linphone provide end-to-end encryption to your Flexisip chat with LIME. We will learn
Linphone provides end-to-end encryption to your Flexisip chat with LIME. We will learn
in this step how to enable and use LIME to secure our ChatRoom. If you want more
informations about LIME take a look to [this page](https://linphone.org/technical-corner/lime).
@ -16,14 +16,11 @@ One-to-one encryption is similar to group chat encryption as it uses LIME. To le
how to create a one-to-one encrypted chat room also see CoreService.cs. You can also try to create one
yourself using the new "Create a new secure ChatRoom" on top of the ChatsPage.
Ephemeral messages are meant to disappear after a certain amount of time. You can enable the ephemeral
mode directly on a ChatRoom object, see ChatPage.xaml.cs to learn how to enable/disable it. And finally
watch the new code in MessageDisplay.xaml(.cs) to see how to handle ephemeral messages.
Ephemeral messages are meant to disappear after a certain amount of time has elapsed.
You can enable the ephemeral mode directly on a ChatRoom object, see ChatPage.xaml.cs to
learn how to enable/disable it. And finally take a look at the new code in MessageDisplay.xaml(.cs)
to see how to handle ephemeral messages.
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 :
@ -41,7 +38,7 @@ New/updated files :
└───Shared :
│ │ ChatRoomToStringConverter.cs : a class that implement IValueConverter to display the
│ │ the chat room name according to its type. Now we display a SECURE tag
│ │ for secure chat room
│ │ for secure chat rooms
│ │
└───Views :
│ │

View File

@ -35,15 +35,7 @@ namespace _07_AdvancedChat.Service
{
private Timer Timer;
private static readonly CoreService instance = new CoreService();
public static CoreService Instance
{
get
{
return instance;
}
}
public static CoreService Instance { get; } = new CoreService();
private Core core;
@ -173,9 +165,6 @@ namespace _07_AdvancedChat.Service
accountParams.RegisterEnabled = true;
// If you want to create some group chats (conferences) you need to
// specify a conference factory URI. Here is the Linphone.org conference
// factory URI.
accountParams.ConferenceFactoryUri = "sip:conference-factory@sip.linphone.org";
Account account = Core.CreateAccount(accountParams);
@ -208,17 +197,17 @@ namespace _07_AdvancedChat.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();
@ -237,22 +226,22 @@ namespace _07_AdvancedChat.Service
Address localAdress = Core.DefaultProxyConfig.IdentityAddress;
ChatRoomParams chatRoomParams = Core.CreateDefaultChatRoomParams();
// To create a one-to-one encrypted chat room we still put GroupEnabled to false.
// To create a one-to-one encrypted chat room we still set GroupEnabled to false...
chatRoomParams.GroupEnabled = false;
chatRoomParams.RttEnabled = false;
if (isSecure)
{
// But here are the things that differ from a basic chat room.
// ...But here are the things that differ from a basic chat room.
// You must use a Flexisip backend,
chatRoomParams.Backend = ChatRoomBackend.FlexisipChat;
// enable encryption and choose your type of encryption backend,
chatRoomParams.EncryptionBackend = ChatRoomEncryptionBackend.Lime;
chatRoomParams.EncryptionEnabled = isSecure;
chatRoomParams.EncryptionEnabled = true;
// and you must set a subject. But often for one-to-one chat rooms the client
// don't use or display the subject (see ChatRoomToStringConverter), so you can
// doesn't display the subject (see ChatRoomToStringConverter), so you can
// put a hard coded subject.
chatRoomParams.Subject = "Dummy Subject";
}
@ -260,7 +249,7 @@ namespace _07_AdvancedChat.Service
{
chatRoomParams.Backend = ChatRoomBackend.Basic;
chatRoomParams.EncryptionBackend = ChatRoomEncryptionBackend.None;
chatRoomParams.EncryptionEnabled = isSecure;
chatRoomParams.EncryptionEnabled = false;
}
return Core.CreateChatRoom(chatRoomParams, localAdress, new[] { remoteAddress });
@ -279,18 +268,18 @@ namespace _07_AdvancedChat.Service
if (isSecure)
{
// If you want to use encryption you should be in a chat room with a Flexisip
// backend, this why we offer the option only on the group chat room for now.
// backend, this is why we offer the option only on the group chat room for now.
// Then simply choose your encryption backend (only LIME is available by default)
chatRoomParams.EncryptionBackend = ChatRoomEncryptionBackend.Lime;
// and put EncryptionEnabled to true.
chatRoomParams.EncryptionEnabled = isSecure;
chatRoomParams.EncryptionEnabled = true;
// Simply doing this you have now a group chat room with end-to-end encryption.
// And just like that, you now have a group chat room with end-to-end encryption.
}
else
{
chatRoomParams.EncryptionBackend = ChatRoomEncryptionBackend.None;
chatRoomParams.EncryptionEnabled = isSecure;
chatRoomParams.EncryptionEnabled = false;
}
return Core.CreateChatRoom(chatRoomParams, localAdress, participants);
@ -307,13 +296,10 @@ namespace _07_AdvancedChat.Service
content.Type = splittedMimeType[0];
content.Subtype = splittedMimeType[1];
// Set the file name for the receiver
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);
@ -328,11 +314,18 @@ namespace _07_AdvancedChat.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

@ -31,7 +31,7 @@ namespace _07_AdvancedChat.Shared
nameInList += " #SECURE#";
}
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 _07_AdvancedChat.Views
private VideoService VideoService { get; } = VideoService.Instance;
private Call IncommingCall;
private Call IncomingCall;
public CallsPage()
{
@ -63,14 +63,14 @@ namespace _07_AdvancedChat.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 _07_AdvancedChat.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 _07_AdvancedChat.Views
}
}
private void AnswerClick(object sender, RoutedEventArgs e)
async private 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 _07_AdvancedChat.Views
{
case CallState.IncomingReceived:
IncommingCall = call;
IncomingCall = call;
IncomingCallStackPanel.Visibility = Visibility.Visible;
IncommingCallText.Text = " " + call.RemoteAddress.AsString();
break;
@ -130,7 +131,7 @@ namespace _07_AdvancedChat.Views
case CallState.OutgoingProgress:
case CallState.OutgoingRinging:
HangOut.IsEnabled = true;
HangUp.IsEnabled = true;
break;
case CallState.StreamsRunning:
@ -151,7 +152,7 @@ namespace _07_AdvancedChat.Views
case CallState.End:
case CallState.Released:
IncommingCall = null;
IncomingCall = null;
EndingCallGuiUpdates();
VideoService.StopVideoStream();
break;
@ -178,7 +179,7 @@ namespace _07_AdvancedChat.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 _07_AdvancedChat.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

@ -45,7 +45,7 @@ namespace _07_AdvancedChat.Views
{
base.OnNavigatedTo(e);
ChatRoom = ((ChatRoom)e.Parameter);
ChatRoom = (ChatRoom)e.Parameter;
ChatRoom.Listener.OnMessageReceived += OnMessageReceived;
ChatRoom.Listener.OnConferenceLeft += AddEvent;
@ -58,24 +58,27 @@ namespace _07_AdvancedChat.Views
ChatRoom.Listener.OnEphemeralEvent += AddEvent;
// In this step we want to test the ephemeral messages. This feature is only
// available if you are using in a Flexisip backend.
if (ChatRoomBackend.FlexisipChat.Equals(ChatRoom.CurrentParams.Backend) && !ChatRoom.EphemeralEnabled)
// available if you are using a Flexisip backend.
if (ChatRoom.CurrentParams.Backend == ChatRoomBackend.FlexisipChat)
{
// In this step when the ephemeral feature is available we enable it
// by default. Just set the EphemeralEnabled to true to activate the
// ephemeral mode, after this point every message send will be marked
// as ephemeral
ChatRoom.EphemeralEnabled = true;
if (!ChatRoom.EphemeralEnabled)
{
// In this step when the ephemeral feature is available we enable it
// by default. Just set the EphemeralEnabled to true to activate the
// ephemeral mode, after this point every message sent will be marked
// as ephemeral
ChatRoom.EphemeralEnabled = true;
// You can choose how many times you want the message to be displayed before
// getting destroyed with EphemeralLifetime, the value is in second.
// Here we set a low value for testing purpose.
ChatRoom.EphemeralLifetime = 15;
// You can choose how long you want the message to be displayed before
// getting destroyed with EphemeralLifetime, the value is in second.
// Here we set a low value for testing purpose.
ChatRoom.EphemeralLifetime = 15;
// At any time you can choose to disable the ephemeral mode moving EphemeralEnabled to false.
// See the EphemeralCheckBox_Unchecked method.
// You can choose to disable the ephemeral mode at any time by setting EphemeralEnabled to false.
// See the EphemeralCheckBox_Unchecked method.
}
}
else if (!ChatRoomBackend.FlexisipChat.Equals(ChatRoom.CurrentParams.Backend))
else
{
// If the backend is not a Flexisip one we hide the ephemeral feature in the ChatPage
// because it doesn't support it.
@ -117,7 +120,7 @@ namespace _07_AdvancedChat.Views
private void UpdateGUI()
{
ChatHeaderText.Text = "Your conversation with : " + ChatRoom.PeerAddress.Username;
foreach (EventLog eventLog in ChatRoom.GetHistoryEvents(0))
foreach (var eventLog in ChatRoom.GetHistoryEvents(0))
{
if (EventLogType.ConferenceChatMessage.Equals(eventLog.Type))
{
@ -137,7 +140,7 @@ namespace _07_AdvancedChat.Views
PeerUsername.Text += ChatRoom.PeerAddress.Username;
YourUsername.Text += ChatRoom.LocalAddress.Username;
// We only display the GroupChatDisplay if we are in real group chat,
// We only display the GroupChatDisplay if we are in a real group chat,
// we don't if it's a OneToOne secure ChatRoom.
if (ChatRoom.HasCapability((int)ChatRoomCapabilities.Conference)
&& !ChatRoom.HasCapability((int)ChatRoomCapabilities.OneToOne))

View File

@ -87,9 +87,9 @@ namespace _07_AdvancedChat.Views
private async void NewChatRoom_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
string peerSipAddress = await Utils.InputTextDialogAsync("Enter peer sip address");
bool secure = Boolean.Parse((string)((Button)sender).Tag);
if (!String.IsNullOrWhiteSpace(peerSipAddress))
if (!string.IsNullOrWhiteSpace(peerSipAddress))
{
bool secure = bool.Parse((string)((Button)sender).Tag); // NEW!
ChatRoom newChatRoom = CoreService.CreateOrGetChatRoom(peerSipAddress, secure);
if (newChatRoom != null)
{

View File

@ -28,7 +28,8 @@ using Windows.UI.Xaml.Navigation;
namespace _07_AdvancedChat.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 _07_AdvancedChat.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 _07_AdvancedChat.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;
}
@ -91,20 +89,13 @@ namespace _07_AdvancedChat.Views
Content = "There is 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));
}
else
{
AppNavFrame.Navigate(typeof(ChatsPage));
}
_ = args.InvokedItem is string invokedItemValue && invokedItemValue.Contains("Calls")
? AppNavFrame.Navigate(typeof(CallsPage))
: AppNavFrame.Navigate(typeof(ChatsPage));
}
private void SignOut_Tapped(object sender, TappedRoutedEventArgs e)
@ -129,7 +120,7 @@ namespace _07_AdvancedChat.Views
CoreService.Core.TerminateAllCalls();
CoreService.LogOut();
this.Frame.Navigate(typeof(LoginPage));
_ = Frame.Navigate(typeof(LoginPage));
}
}

View File

@ -1,22 +1,34 @@
Linphone X UWP tutorial
=======================
# Linphone X UWP tutorial
Welcome to the C# tutorial, we are going to learn how to use the LinphoneSDK in the UWP (Universal Windows Platform) environment.
Welcome to the C# tutorial, we are going to learn how to use the Linphone SDK in a UWP (Universal Windows Platform) environment.
We recommend you to use Visual Studio 2019 to follow this tutorial.
We recommend you use Visual Studio 2019 to follow this tutorial.
First of all you can open the TutorialsCS solution in Visual Studio (TutorialsCS.sln) .
Under the solution you will find several projects, each is a step from a hello world to a nearly full communication app using LinphoneSDK.
Each step work and can be run independently, and in each step the new "Linphone" code is explained.
## Prerequisite: Installing the LinphoneSDK NuGet package
Don't forget to get a look at [Liblinphone doc](https://linphone.org/releases/docs/liblinphone/5.0/cs/) to see the complete list of available APIs.
Before you start and if you haven't done so already, you should download and install the LinphoneSDK NuGet package.
You can find a readme file in each project, explaining the project goal and how to run and understand it.
- [00_HelloWorld](00_HelloWorld/) : A simple "Hello World" with Linphone version number.
- [01_AccountLogin](01_AccountLogin/) : Learn how to login to your SIP account.
- [02_IncomingCall](02_IncomingCall/) : Receive calls.
- [03_OutgoingCall](03_OutgoingCall/) : Make your first calls.
- [04_BasicChat](04_BasicChat/) : Learn how to manage and send message in basic chat rooms.
- [05_FileTransfer](05_FileTransfer/) : Send files in your previously created chat rooms.
- [06_GroupChat](06_GroupChat/) : Create and manage group chat rooms.
- [07_AdvancedChat](07_AdvancedChat/) : Secure chat rooms, ephemeral messages.
1. Choose a local folder (e.g. `Downloads`) and set it as a NuGet source.
This will be where you put the downloaded .nupkg files.
In Visual Studio: Tools > NuGet Package Manager > Package Manager Settings > Package Sources > > Source: ...
2. Download `LinphoneSDK.5.1.0.nupkg` from https://www.linphone.org/snapshots/windows/sdk/ to the folder you chose above.
3. Open the TutorialsCS solution in Visual Studio (TutorialsCS.sln).
4. Right-click on the TutorialsCS solution and 'Restore NuGet Packages'.
## Getting Started
Inside the TutorialsCS solution, you will find several projects, each of which is a step from a hello world to a nearly full-featured communication app using the Linphone SDK.
Each step builds upon the previous ones but is standalone and can be run on its own. At each step, only new Linphone-related code is explained.
You will find additional Readme files in each project for further details.
- [00_HelloWorld](00_HelloWorld/) : A simple "Hello World" to display the Linphone version number.
- [01_AccountLogin](01_AccountLogin/) : Learn how to login to your SIP account.
- [02_IncomingCall](02_IncomingCall/) : Receive calls.
- [03_OutgoingCall](03_OutgoingCall/) : Make your first calls.
- [04_BasicChat](04_BasicChat/) : Learn how to manage and send message in basic chat rooms.
- [05_FileTransfer](05_FileTransfer/) : Send files in your previously created chat rooms.
- [06_GroupChat](06_GroupChat/) : Create and manage group chat rooms.
- [07_AdvancedChat](07_AdvancedChat/) : Secure chat rooms, ephemeral messages.
To complement this tutorial and to get the complete list of available APIs, take a look at [the Liblinphone documentation](https://linphone.org/releases/docs/liblinphone/5.0/cs/).

View File

@ -15,6 +15,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "05_FileTransfer", "05_FileT
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{248A2D49-EC1F-4957-B6C4-E88A0D854CE6}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Readme.md = Readme.md
EndProjectSection
EndProject