From acc4c413925d255591f1c7dbd5e86a815789957c Mon Sep 17 00:00:00 2001 From: atraczyk <andreastraczyk@gmail.com> Date: Thu, 10 Nov 2016 22:56:52 -0500 Subject: [PATCH] settings: add the ability to modify video device settings - adds the ability to select device, resolution, and frame rate - modifies the initialization of the daemon by seperating the registration of the callbacks from the init function and places the start and run loop in an IAsyncAction worker thread with forced high priority - uses std::ofstream for debug log file instead of Platform functions Change-Id: I32439088fe58513c46d11297db4898ca237174e7 Tuleap: #790 --- LoadingPage.xaml | 5 +- MainPage.xaml | 4 + MainPage.xaml.cpp | 68 ++-- MainPage.xaml.h | 2 + MessageTextPage.xaml | 20 +- Package.appxmanifest | 2 +- PreviewPage.xaml | 37 +++ PreviewPage.xaml.cpp | 37 +++ PreviewPage.xaml.h | 34 ++ RingD.cpp | 572 ++++++++++++++++---------------- RingD.h | 16 + RingDebug.cpp | 28 +- RingDebug.h | 3 - SmartPanel.xaml | 25 +- SmartPanel.xaml.cpp | 172 +++++++++- SmartPanel.xaml.h | 10 + Utils.h | 136 ++++---- Video.cpp | 188 +++++------ Video.h | 117 +++---- VideoCaptureManager.cpp | 270 ++++++++++----- VideoCaptureManager.h | 21 +- VideoPage.xaml | 20 +- VideoPage.xaml.cpp | 5 +- VideoPage.xaml.h | 3 +- WelcomePage.xaml | 1 + WelcomePage.xaml.cpp | 1 + WelcomePage.xaml.h | 3 +- _language-fr.appx | Bin 4148 -> 4162 bytes _scale-100.appx | Bin 29188 -> 29196 bytes _scale-125.appx | Bin 30289 -> 30293 bytes _scale-150.appx | Bin 31201 -> 31206 bytes _scale-400.appx | Bin 43153 -> 43161 bytes ring-client-uwp.vcxproj | 7 + ring-client-uwp.vcxproj.filters | 3 + 34 files changed, 1143 insertions(+), 667 deletions(-) create mode 100644 PreviewPage.xaml create mode 100644 PreviewPage.xaml.cpp create mode 100644 PreviewPage.xaml.h diff --git a/LoadingPage.xaml b/LoadingPage.xaml index 72fa68c..9d6641f 100644 --- a/LoadingPage.xaml +++ b/LoadingPage.xaml @@ -1,6 +1,7 @@ <!-- ********************************************************************** * Copyright (C) 2016 by Savoir-faire Linux * * Author: Jäger Nicolas<nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas<andreas.traczyk@savoirfairelinux.com> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -23,8 +24,4 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" NavigationCacheMode="Enabled" mc:Ignorable="d"> - - <Grid> - <TextBlock Text="loading page"/> - </Grid> </Page> diff --git a/MainPage.xaml b/MainPage.xaml index 3aae9ae..673aef6 100644 --- a/MainPage.xaml +++ b/MainPage.xaml @@ -120,6 +120,10 @@ <Frame x:Name="_videoFrame_" Grid.Row="0" Visibility="Visible"/> + <Frame x:Name="_previewFrame_" + Grid.Row="1" + Canvas.ZIndex="99" + Visibility="Collapsed"/> </Grid> </SplitView.Content> </SplitView> diff --git a/MainPage.xaml.cpp b/MainPage.xaml.cpp index 1833453..e9805bb 100644 --- a/MainPage.xaml.cpp +++ b/MainPage.xaml.cpp @@ -22,8 +22,11 @@ #include "SmartPanel.xaml.h" #include "RingConsolePanel.xaml.h" #include "VideoPage.xaml.h" +#include "PreviewPage.xaml.h" #include "WelcomePage.xaml.h" +#include "gnutls\gnutls.h" + #include "MainPage.xaml.h" using namespace RingClientUWP; @@ -60,6 +63,7 @@ MainPage::MainPage() _smartPanel_->Navigate(TypeName(RingClientUWP::Views::SmartPanel::typeid)); _consolePanel_->Navigate(TypeName(RingClientUWP::Views::RingConsolePanel::typeid)); _videoFrame_->Navigate(TypeName(RingClientUWP::Views::VideoPage::typeid)); + _previewFrame_->Navigate(TypeName(RingClientUWP::Views::PreviewPage::typeid)); _messageTextFrame_->Navigate(TypeName(RingClientUWP::Views::MessageTextPage::typeid)); /* connect to delegates */ @@ -67,6 +71,8 @@ MainPage::MainPage() auto smartPanel = dynamic_cast<SmartPanel^>(_smartPanel_->Content); smartPanel->summonMessageTextPage += ref new RingClientUWP::SummonMessageTextPage(this, &RingClientUWP::MainPage::OnsummonMessageTextPage); smartPanel->summonWelcomePage += ref new RingClientUWP::SummonWelcomePage(this, &RingClientUWP::MainPage::OnsummonWelcomePage); + smartPanel->summonPreviewPage += ref new RingClientUWP::SummonPreviewPage(this, &RingClientUWP::MainPage::OnsummonPreviewPage); + smartPanel->hidePreviewPage += ref new RingClientUWP::HidePreviewPage(this, &RingClientUWP::MainPage::OnhidePreviewPage); smartPanel->summonVideoPage += ref new RingClientUWP::SummonVideoPage(this, &RingClientUWP::MainPage::OnsummonVideoPage); auto videoPage = dynamic_cast<VideoPage^>(_videoFrame_->Content); videoPage->pressHangUpCall += ref new RingClientUWP::PressHangUpCall(this, &RingClientUWP::MainPage::OnpressHangUpCall); @@ -127,7 +133,10 @@ RingClientUWP::MainPage::showFrame(Windows::UI::Xaml::Controls::Frame^ frame) void RingClientUWP::MainPage::OnNavigatedTo(NavigationEventArgs ^ e) { - RingD::instance->startDaemon(); + gnutls_global_init(); + RingD::instance->registerCallbacks(); + RingD::instance->initDaemon( DRing::DRING_FLAG_CONSOLE_LOG | DRing::DRING_FLAG_DEBUG ); + Video::VideoManager::instance->captureManager()->EnumerateWebcamsAsync(); showLoadingOverlay(true, false); } @@ -231,6 +240,17 @@ void RingClientUWP::MainPage::OnsummonWelcomePage() showFrame(_welcomeFrame_); } +void RingClientUWP::MainPage::OnsummonPreviewPage() +{ + WriteLine("Show Settings Preview"); + _previewFrame_->Visibility = VIS::Visible; +} + +void RingClientUWP::MainPage::OnhidePreviewPage() +{ + WriteLine("Hide Settings Preview"); + _previewFrame_->Visibility = VIS::Collapsed; +} void RingClientUWP::MainPage::OnsummonVideoPage() { @@ -245,8 +265,6 @@ void RingClientUWP::MainPage::OnpressHangUpCall() OnsummonMessageTextPage(); } - - void RingClientUWP::MainPage::OnstateChange(Platform::String ^callId, RingClientUWP::CallStatus state, int code) { auto item = SmartPanelItemsViewModel::instance->_selectedItem; @@ -264,8 +282,7 @@ void RingClientUWP::MainPage::OnstateChange(Platform::String ^callId, RingClient } } -#include <dring.h> -#include "callmanager_interface.h" + void MainPage::Application_Suspending(Object^, Windows::ApplicationModel::SuspendingEventArgs^ e) { @@ -299,9 +316,9 @@ MainPage::Application_Suspending(Object^, Windows::ApplicationModel::SuspendingE void MainPage::Application_VisibilityChanged(Object^ sender, VisibilityChangedEventArgs^ e) { + auto vcm = Video::VideoManager::instance->captureManager(); if (e->Visible) { RingDebug::instance->WriteLine("->Visible"); - auto isPreviewing = Video::VideoManager::instance->captureManager()->isPreviewing; bool isInCall = false; for (auto item : SmartPanelItemsViewModel::instance->itemsList) { if (item->_callId && item->_callStatus == CallStatus::IN_PROGRESS) { @@ -312,13 +329,23 @@ MainPage::Application_VisibilityChanged(Object^ sender, VisibilityChangedEventAr if (isInCall) { /*if (RingD::instance->currentCallId) RingD::instance->unPauseCall(RingD::instance->currentCallId);*/ - Video::VideoManager::instance->captureManager()->InitializeCameraAsync(); - Video::VideoManager::instance->captureManager()->videoFrameCopyInvoker->Start(); + vcm->InitializeCameraAsync(false); + vcm->videoFrameCopyInvoker->Start(); + } + else if (vcm->isSettingsPreviewing) { + vcm->CleanupCameraAsync() + .then([=](task<void> cleanupTask){ + cleanupTask.get(); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::High, ref new DispatchedHandler([=]() + { + vcm->InitializeCameraAsync(true); + })); + }); } } else { RingDebug::instance->WriteLine("->Invisible"); - auto isPreviewing = Video::VideoManager::instance->captureManager()->isPreviewing; bool isInCall = false; for (auto item : SmartPanelItemsViewModel::instance->itemsList) { if (item->_callId && item->_callStatus == CallStatus::IN_PROGRESS) { @@ -328,17 +355,21 @@ MainPage::Application_VisibilityChanged(Object^ sender, VisibilityChangedEventAr } } if (isInCall) { + // TODO /*if (RingD::instance->currentCallId) { WriteLine("Pausing call: " + RingD::instance->currentCallId); RingD::instance->pauseCall(RingD::instance->currentCallId); }*/ - if (isPreviewing) { - Video::VideoManager::instance->captureManager()->StopPreviewAsync(); - if (Video::VideoManager::instance->captureManager()->captureTaskTokenSource) - Video::VideoManager::instance->captureManager()->captureTaskTokenSource->cancel(); - Video::VideoManager::instance->captureManager()->videoFrameCopyInvoker->Stop(); + if (vcm->isPreviewing) { + vcm->StopPreviewAsync(); + if (vcm->captureTaskTokenSource) + vcm->captureTaskTokenSource->cancel(); + vcm->videoFrameCopyInvoker->Stop(); } } + else if (vcm->isSettingsPreviewing) { + vcm->StopPreviewAsync(); + } } } @@ -383,11 +414,10 @@ MainPage::BeginExtendedExecution() session = newSession; RingDebug::instance->WriteLine("Request Extended Execution Allowed"); RingDebug::instance->WriteLine("Clean up camera..."); - Video::VideoManager::instance->captureManager()->CleanupCameraAsync() - .then([]() { - RingDebug::instance->WriteLine("Hang up calls..."); - DRing::fini(); - }); + Video::VideoManager::instance->captureManager()->CleanupCameraAsync(); + RingDebug::instance->WriteLine("Hang up calls..."); + DRing::fini(); + gnutls_global_init(); break; default: diff --git a/MainPage.xaml.h b/MainPage.xaml.h index 821efbf..7252ac3 100644 --- a/MainPage.xaml.h +++ b/MainPage.xaml.h @@ -71,6 +71,8 @@ private: void showFrame(Windows::UI::Xaml::Controls::Frame^ frame); void OnsummonMessageTextPage(); void OnsummonWelcomePage(); + void OnsummonPreviewPage(); + void OnhidePreviewPage(); void OnsummonVideoPage(); void OnpressHangUpCall(); void OnstateChange(Platform::String ^callId, CallStatus state, int code); diff --git a/MessageTextPage.xaml b/MessageTextPage.xaml index 7c5562c..e72ea6f 100644 --- a/MessageTextPage.xaml +++ b/MessageTextPage.xaml @@ -1,4 +1,22 @@ -<Page +<!-- ********************************************************************** +* Copyright (C) 2016 by Savoir-faire Linux * +* Author: Jäger Nicolas<nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas<andreas.traczyk@savoirfairelinux.com> * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 3 of the License, or * +* (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program. If not, see <http://www.gnu.org/licenses/> . * +*********************************************************************** --> +<Page x:Class="RingClientUWP.Views.MessageTextPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" diff --git a/Package.appxmanifest b/Package.appxmanifest index c188372..7f83564 100644 --- a/Package.appxmanifest +++ b/Package.appxmanifest @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp"> - <Identity Name="Savoir-faireLinux.GNURing" Publisher="CN=8121A5F7-3CA1-4CAA-92B2-4F595B011941" Version="1.1.6.0" /> + <Identity Name="Savoir-faireLinux.GNURing" Publisher="CN=8121A5F7-3CA1-4CAA-92B2-4F595B011941" Version="1.1.10.0" /> <mp:PhoneIdentity PhoneProductId="2385953f-9019-423d-aa82-d1bbacfa258b" PhonePublisherId="00000000-0000-0000-0000-000000000000" /> <Properties> <DisplayName>GNU Ring</DisplayName> diff --git a/PreviewPage.xaml b/PreviewPage.xaml new file mode 100644 index 0000000..c043296 --- /dev/null +++ b/PreviewPage.xaml @@ -0,0 +1,37 @@ +<!-- ********************************************************************** +* Copyright (C) 2016 by Savoir-faire Linux * +* Author: Jäger Nicolas<nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas<andreas.traczyk@savoirfairelinux.com> * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 3 of the License, or * +* (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program. If not, see <http://www.gnu.org/licenses/> . * +*********************************************************************** --> +<Page x:Class="RingClientUWP.Views.PreviewPage" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:RingClientUWP" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + NavigationCacheMode="Enabled" + mc:Ignorable="d"> + + <Grid x:Name="_previewPage_" + Background="Black"> + <!--camera preview--> + <CaptureElement Name="SettingsPreviewImage" + VerticalAlignment="Center" + HorizontalAlignment="Center" + Stretch="Uniform"/> + </Grid> + +</Page> diff --git a/PreviewPage.xaml.cpp b/PreviewPage.xaml.cpp new file mode 100644 index 0000000..6a89f58 --- /dev/null +++ b/PreviewPage.xaml.cpp @@ -0,0 +1,37 @@ +/************************************************************************** +* Copyright (C) 2016 by Savoir-faire Linux * +* Author: J�ger Nicolas <nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas <andreas.traczyk@savoirfairelinux.com> * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 3 of the License, or * +* (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program. If not, see <http://www.gnu.org/licenses/>. * +**************************************************************************/ +#include "pch.h" + +#include "PreviewPage.xaml.h" + +using namespace RingClientUWP; +using namespace RingClientUWP::Views; + +using namespace Video; + +PreviewPage::PreviewPage() +{ + InitializeComponent(); + + VideoManager::instance->captureManager()->getSettingsPreviewSink += + ref new GetSettingsPreviewSink([this]() + { + return SettingsPreviewImage; + }); +}; \ No newline at end of file diff --git a/PreviewPage.xaml.h b/PreviewPage.xaml.h new file mode 100644 index 0000000..ef6270d --- /dev/null +++ b/PreviewPage.xaml.h @@ -0,0 +1,34 @@ +/************************************************************************** +* Copyright (C) 2016 by Savoir-faire Linux * +* Author: J�ger Nicolas <nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas <andreas.traczyk@savoirfairelinux.com> * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 3 of the License, or * +* (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program. If not, see <http://www.gnu.org/licenses/>. * +**************************************************************************/ +#pragma once + +#include "PreviewPage.g.h" + +namespace RingClientUWP +{ +namespace Views +{ +public ref class PreviewPage sealed +{ +public: + PreviewPage(); +protected: +}; +} +} \ No newline at end of file diff --git a/RingD.cpp b/RingD.cpp index 8495a12..4bab22f 100644 --- a/RingD.cpp +++ b/RingD.cpp @@ -18,8 +18,6 @@ **************************************************************************/ #include "pch.h" -/* daemon */ -#include <dring.h> #include "callmanager_interface.h" #include "configurationmanager_interface.h" #include "presencemanager_interface.h" @@ -39,6 +37,7 @@ using namespace Windows::UI::Core; using namespace Windows::Media; using namespace Windows::Media::MediaProperties; using namespace Windows::Media::Capture; +using namespace Windows::System::Threading; using namespace RingClientUWP; using namespace RingClientUWP::Utils; @@ -116,8 +115,6 @@ RingClientUWP::RingD::reloadAccountList() } } - DRing::lookupName("", "", "wagaf"); - // load user preferences Configuration::UserPreferences::instance->load(); } @@ -365,150 +362,124 @@ void RingClientUWP::RingD::registerThisDevice(String ^ pin, String ^ archivePass } void -RingClientUWP::RingD::startDaemon() +RingD::registerCallbacks() { - if (daemonRunning) { - ERR_("daemon already runnging"); - return; - } - //eraseCacheFolder(); - editModeOn_ = true; + dispatcher = CoreApplication::MainView->CoreWindow->Dispatcher; + + callHandlers = { + // use IncomingCall only to register the call client sided, use StateChange to determine the impact on the UI + DRing::exportable_callback<DRing::CallSignal::IncomingCall>([this]( + const std::string& accountId, + const std::string& callId, + const std::string& from) + { + MSG_("<IncomingCall>"); + MSG_("accountId = " + accountId); + MSG_("callId = " + callId); + MSG_("from = " + from); - create_task([&]() - { - using SharedCallback = std::shared_ptr<DRing::CallbackWrapperBase>; - using namespace std::placeholders; + auto accountId2 = toPlatformString(accountId); + auto callId2 = toPlatformString(callId); + auto from2 = toPlatformString(from); - auto dispatcher = CoreApplication::MainView->CoreWindow->Dispatcher; + /* fix some issue in the daemon --> <...@...> */ + from2 = Utils::TrimRingId(from2); - std::map<std::string, SharedCallback> callHandlers = { - // use IncomingCall only to register the call client sided, use StateChange to determine the impact on the UI - DRing::exportable_callback<DRing::CallSignal::IncomingCall>([this]( - const std::string& accountId, - const std::string& callId, - const std::string& from) + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::High, ref new DispatchedHandler([=]() { - MSG_("<IncomingCall>"); - MSG_("accountId = " + accountId); - MSG_("callId = " + callId); - MSG_("from = " + from); + incomingCall(accountId2, callId2, from2); + stateChange(callId2, CallStatus::INCOMING_RINGING, 0); - auto accountId2 = toPlatformString(accountId); - auto callId2 = toPlatformString(callId); - auto from2 = toPlatformString(from); + auto contact = ContactsViewModel::instance->findContactByName(from2); + auto item = SmartPanelItemsViewModel::instance->findItem(contact); + item->_callId = callId2; + })); + }), + DRing::exportable_callback<DRing::CallSignal::StateChange>([this]( + const std::string& callId, + const std::string& state, + int code) + { + MSG_("<StateChange>"); + MSG_("callId = " + callId); + MSG_("state = " + state); + MSG_("code = " + std::to_string(code)); - /* fix some issue in the daemon --> <...@...> */ - from2 = Utils::TrimRingId(from2); + auto callId2 = toPlatformString(callId); + auto state2 = toPlatformString(state); - CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - CoreDispatcherPriority::High, ref new DispatchedHandler([=]() - { - incomingCall(accountId2, callId2, from2); - stateChange(callId2, CallStatus::INCOMING_RINGING, 0); + auto state3 = translateCallStatus(state2); - auto contact = ContactsViewModel::instance->findContactByName(from2); - auto item = SmartPanelItemsViewModel::instance->findItem(contact); - item->_callId = callId2; - })); - }), - DRing::exportable_callback<DRing::CallSignal::StateChange>([this]( - const std::string& callId, - const std::string& state, - int code) - { - MSG_("<StateChange>"); - MSG_("callId = " + callId); - MSG_("state = " + state); - MSG_("code = " + std::to_string(code)); + if (state3 == CallStatus::ENDED) + DRing::hangUp(callId); // solve a bug in the daemon API. - auto callId2 = toPlatformString(callId); - auto state2 = toPlatformString(state); - - auto state3 = translateCallStatus(state2); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::High, ref new DispatchedHandler([=]() + { + stateChange(callId2, state3, code); + })); + }), + DRing::exportable_callback<DRing::ConfigurationSignal::IncomingAccountMessage>([&]( + const std::string& accountId, + const std::string& from, + const std::map<std::string, std::string>& payloads) + { + MSG_("<IncomingAccountMessage>"); + MSG_("accountId = " + accountId); + MSG_("from = " + from); - if (state3 == CallStatus::ENDED) - DRing::hangUp(callId); // solve a bug in the daemon API. + auto accountId2 = toPlatformString(accountId); + auto from2 = toPlatformString(from); + for (auto i : payloads) { + MSG_("payload = " + i.second); + auto payload = Utils::toPlatformString(i.second); CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( CoreDispatcherPriority::High, ref new DispatchedHandler([=]() { - stateChange(callId2, state3, code); + incomingAccountMessage(accountId2, from2, payload); })); - }), - DRing::exportable_callback<DRing::ConfigurationSignal::IncomingAccountMessage>([&]( - const std::string& accountId, - const std::string& from, - const std::map<std::string, std::string>& payloads) - { - MSG_("<IncomingAccountMessage>"); - MSG_("accountId = " + accountId); - MSG_("from = " + from); - - auto accountId2 = toPlatformString(accountId); - auto from2 = toPlatformString(from); - - for (auto i : payloads) { - MSG_("payload = " + i.second); - auto payload = Utils::toPlatformString(i.second); - CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - CoreDispatcherPriority::High, ref new DispatchedHandler([=]() - { - incomingAccountMessage(accountId2, from2, payload); - })); - } - }), - DRing::exportable_callback<DRing::CallSignal::IncomingMessage>([&]( - const std::string& callId, - const std::string& from, - const std::map<std::string, std::string>& payloads) - { - MSG_("<IncomingMessage>"); - MSG_("callId = " + callId); - MSG_("from = " + from); + } + }), + DRing::exportable_callback<DRing::CallSignal::IncomingMessage>([&]( + const std::string& callId, + const std::string& from, + const std::map<std::string, std::string>& payloads) + { + MSG_("<IncomingMessage>"); + MSG_("callId = " + callId); + MSG_("from = " + from); - auto callId2 = toPlatformString(callId); - auto from2 = toPlatformString(from); + auto callId2 = toPlatformString(callId); + auto from2 = toPlatformString(from); - const std::string PROFILE_VCF = "x-ring/ring.profile.vcard"; - static const unsigned int profileSize = PROFILE_VCF.size(); + const std::string PROFILE_VCF = "x-ring/ring.profile.vcard"; + static const unsigned int profileSize = PROFILE_VCF.size(); - for (auto i : payloads) { - if (i.first.compare(0, profileSize, PROFILE_VCF) == 0) { - MSG_("VCARD"); - return; - } - MSG_("payload.first = " + i.first); - MSG_("payload.second = " + i.second); - - auto payload = Utils::toPlatformString(i.second); - CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - CoreDispatcherPriority::High, ref new DispatchedHandler([=]() - { - incomingMessage(callId2, payload); - MSG_("message recu :" + i.second); - })); + for (auto i : payloads) { + if (i.first.compare(0, profileSize, PROFILE_VCF) == 0) { + MSG_("VCARD"); + return; } - }), - DRing::exportable_callback<DRing::ConfigurationSignal::RegistrationStateChanged>([this]( - const std::string& account_id, const std::string& state, - int detailsCode, const std::string& detailsStr) - { - MSG_("<RegistrationStateChanged>: ID = " + account_id + " state = " + state); - if (state == DRing::Account::States::REGISTERED) { - CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - reloadAccountList(); - if (editModeOn_) { - auto frame = dynamic_cast<Frame^>(Window::Current->Content); - dynamic_cast<RingClientUWP::MainPage^>(frame->Content)->showLoadingOverlay(false, false); - editModeOn_ = false; - } - })); - } - }), - DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([this]() - { - MSG_("<AccountsChanged>"); + MSG_("payload.first = " + i.first); + MSG_("payload.second = " + i.second); + + auto payload = Utils::toPlatformString(i.second); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::High, ref new DispatchedHandler([=]() + { + incomingMessage(callId2, payload); + MSG_("message recu :" + i.second); + })); + } + }), + DRing::exportable_callback<DRing::ConfigurationSignal::RegistrationStateChanged>([this]( + const std::string& account_id, const std::string& state, + int detailsCode, const std::string& detailsStr) + { + MSG_("<RegistrationStateChanged>: ID = " + account_id + " state = " + state); + if (state == DRing::Account::States::REGISTERED) { CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::High, ref new DispatchedHandler([=]() { reloadAccountList(); @@ -518,165 +489,214 @@ RingClientUWP::RingD::startDaemon() editModeOn_ = false; } })); - }), - DRing::exportable_callback<DRing::Debug::MessageSend>([&](const std::string& toto) - { - if (debugModeOn_) - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - RingDebug::instance->print(toto); - })); - }), - - - DRing::exportable_callback<DRing::ConfigurationSignal::KnownDevicesChanged>([&](const std::string& accountId, const std::map<std::string, std::string>& devices) - { - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - RingDebug::instance->print("KnownDevicesChanged ---> C PAS FINI"); - })); - }), - DRing::exportable_callback<DRing::ConfigurationSignal::ExportOnRingEnded>([&](const std::string& accountId, int status, const std::string& pin) - { - auto accountId2 = Utils::toPlatformString(accountId); - auto pin2 = (pin.empty()) ? "Error bad password" : "Your generated pin : " + Utils::toPlatformString(pin); - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - exportOnRingEnded(accountId2, pin2); - })); - }) - }; - registerCallHandlers(callHandlers); - - std::map<std::string, SharedCallback> getAppPathHandler = - { - DRing::exportable_callback<DRing::ConfigurationSignal::GetAppDataPath> - ([this](std::vector<std::string>* paths) { - paths->emplace_back(localFolder_); - }) - }; - registerCallHandlers(getAppPathHandler); - - std::map<std::string, SharedCallback> getAppUserNameHandler = + } + }), + DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([this]() { - DRing::exportable_callback<DRing::ConfigurationSignal::GetAppUserName> - ([this](std::vector<std::string>* unames) { - unames->emplace_back(Utils::toString( - UserModel::instance->firstName + - "." + - UserModel::instance->lastName)); - }) - }; - registerCallHandlers(getAppUserNameHandler); - - std::map<std::string, SharedCallback> incomingVideoHandlers = + MSG_("<AccountsChanged>"); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + reloadAccountList(); + if (editModeOn_) { + auto frame = dynamic_cast<Frame^>(Window::Current->Content); + dynamic_cast<RingClientUWP::MainPage^>(frame->Content)->showLoadingOverlay(false, false); + editModeOn_ = false; + } + })); + }), + DRing::exportable_callback<DRing::Debug::MessageSend>([&](const std::string& toto) { - DRing::exportable_callback<DRing::VideoSignal::DeviceEvent> - ([this]() { - MSG_("<DeviceEvent>"); - }), - DRing::exportable_callback<DRing::VideoSignal::DecodingStarted> - ([&](const std::string &id, const std::string &shmPath, int width, int height, bool isMixer) { + if (debugModeOn_) dispatcher->RunAsync(CoreDispatcherPriority::High, ref new DispatchedHandler([=]() { - Video::VideoManager::instance->rendererManager()->startedDecoding( - Utils::toPlatformString(id), - width, - height); - auto callId2 = Utils::toPlatformString(id); - incomingVideoMuted(callId2, false); - })); - }), - DRing::exportable_callback<DRing::VideoSignal::DecodingStopped> - ([&](const std::string &id, const std::string &shmPath, bool isMixer) { - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - Video::VideoManager::instance->rendererManager()->removeRenderer(Utils::toPlatformString(id)); - auto callId2 = Utils::toPlatformString(id); - incomingVideoMuted(callId2, true); - })); - }) - }; - registerVideoHandlers(incomingVideoHandlers); + RingDebug::instance->print(toto); + })); + }), + - using namespace Video; - std::map<std::string, SharedCallback> outgoingVideoHandlers = + DRing::exportable_callback<DRing::ConfigurationSignal::KnownDevicesChanged>([&](const std::string& accountId, const std::map<std::string, std::string>& devices) { - DRing::exportable_callback<DRing::VideoSignal::GetCameraInfo> - ([this](const std::string& device, - std::vector<std::string> *formats, - std::vector<unsigned> *sizes, - std::vector<unsigned> *rates) { - auto device_list = VideoManager::instance->captureManager()->deviceList; - - for (unsigned int i = 0; i < device_list->Size; i++) { - auto dev = device_list->GetAt(i); - if (device == Utils::toString(dev->name())) { - auto channel = dev->channel(); - Vector<Video::Resolution^>^ resolutions = channel->resolutionList(); - for (auto res : resolutions) { - formats->emplace_back(Utils::toString(res->format())); - sizes->emplace_back(res->size()->width()); - sizes->emplace_back(res->size()->height()); - rates->emplace_back(res->activeRate()->value()); + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + RingDebug::instance->print("KnownDevicesChanged ---> C PAS FINI"); + })); + }), + DRing::exportable_callback<DRing::ConfigurationSignal::ExportOnRingEnded>([&](const std::string& accountId, int status, const std::string& pin) + { + auto accountId2 = Utils::toPlatformString(accountId); + auto pin2 = (pin.empty()) ? "Error bad password" : "Your generated pin : " + Utils::toPlatformString(pin); + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + exportOnRingEnded(accountId2, pin2); + })); + }) + }; + registerCallHandlers(callHandlers); + + getAppPathHandler = + { + DRing::exportable_callback<DRing::ConfigurationSignal::GetAppDataPath> + ([this](std::vector<std::string>* paths) { + paths->emplace_back(localFolder_); + }) + }; + registerCallHandlers(getAppPathHandler); + + getAppUserNameHandler = + { + DRing::exportable_callback<DRing::ConfigurationSignal::GetAppUserName> + ([this](std::vector<std::string>* unames) { + unames->emplace_back(Utils::toString( + UserModel::instance->firstName + + "." + + UserModel::instance->lastName)); + }) + }; + registerCallHandlers(getAppUserNameHandler); + + incomingVideoHandlers = + { + DRing::exportable_callback<DRing::VideoSignal::DeviceEvent> + ([this]() { + MSG_("<DeviceEvent>"); + }), + DRing::exportable_callback<DRing::VideoSignal::DecodingStarted> + ([&](const std::string &id, const std::string &shmPath, int width, int height, bool isMixer) { + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + Video::VideoManager::instance->rendererManager()->startedDecoding( + Utils::toPlatformString(id), + width, + height); + auto callId2 = Utils::toPlatformString(id); + incomingVideoMuted(callId2, false); + })); + }), + DRing::exportable_callback<DRing::VideoSignal::DecodingStopped> + ([&](const std::string &id, const std::string &shmPath, bool isMixer) { + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + Video::VideoManager::instance->rendererManager()->removeRenderer(Utils::toPlatformString(id)); + auto callId2 = Utils::toPlatformString(id); + incomingVideoMuted(callId2, true); + })); + }) + }; + registerVideoHandlers(incomingVideoHandlers); + + using namespace Video; + outgoingVideoHandlers = + { + DRing::exportable_callback<DRing::VideoSignal::GetCameraInfo> + ([this](const std::string& device, + std::vector<std::string> *formats, + std::vector<unsigned> *sizes, + std::vector<unsigned> *rates) { + MSG_("<GetCameraInfo>"); + auto device_list = VideoManager::instance->captureManager()->deviceList; + + for (unsigned int i = 0; i < device_list->Size; i++) { + auto dev = device_list->GetAt(i); + if (device == Utils::toString(dev->name())) { + Vector<Video::Resolution^>^ resolutions = dev->resolutionList(); + for (auto res : resolutions) { + formats->emplace_back(Utils::toString(res->activeRate()->format())); + sizes->emplace_back(res->width()); + sizes->emplace_back(res->height()); + for (auto rate : res->rateList()) { + rates->emplace_back(rate->value()); } } } - }), - DRing::exportable_callback<DRing::VideoSignal::SetParameters> - ([&](const std::string& device, - std::string format, - const int width, - const int height, - const int rate) { - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - VideoManager::instance->captureManager()->activeDevice->SetDeviceProperties( - Utils::toPlatformString(format),width,height,rate); - })); - }), - DRing::exportable_callback<DRing::VideoSignal::StartCapture> - ([&](const std::string& device) { - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - VideoManager::instance->captureManager()->InitializeCameraAsync(); - VideoManager::instance->captureManager()->videoFrameCopyInvoker->Start(); - })); - }), - DRing::exportable_callback<DRing::VideoSignal::StopCapture> - ([&]() { - dispatcher->RunAsync(CoreDispatcherPriority::High, - ref new DispatchedHandler([=]() { - VideoManager::instance->captureManager()->StopPreviewAsync(); - if (VideoManager::instance->captureManager()->captureTaskTokenSource) - VideoManager::instance->captureManager()->captureTaskTokenSource->cancel(); - VideoManager::instance->captureManager()->videoFrameCopyInvoker->Stop(); - })); - }) - }; - registerVideoHandlers(outgoingVideoHandlers); - - std::map<std::string, SharedCallback> nameRegistrationHandlers = - { - DRing::exportable_callback<DRing::ConfigurationSignal::NameRegistrationEnded>( - [this](const std::string &accountId, int status, const std::string &name) { - MSG_("\n<NameRegistrationEnded>\n"); + } + }), + DRing::exportable_callback<DRing::VideoSignal::SetParameters> + ([&](const std::string& device, + std::string format, + const int width, + const int height, + const int rate) { + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + MSG_("<SetParameters>"); + VideoManager::instance->captureManager()->activeDevice->SetDeviceProperties( + Utils::toPlatformString(format),width,height,rate); + })); + }), + DRing::exportable_callback<DRing::VideoSignal::StartCapture> + ([&](const std::string& device) { + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + VideoManager::instance->captureManager()->InitializeCameraAsync(false); + VideoManager::instance->captureManager()->videoFrameCopyInvoker->Start(); + })); + }), + DRing::exportable_callback<DRing::VideoSignal::StopCapture> + ([&]() { + dispatcher->RunAsync(CoreDispatcherPriority::High, + ref new DispatchedHandler([=]() { + VideoManager::instance->captureManager()->StopPreviewAsync(); + if (VideoManager::instance->captureManager()->captureTaskTokenSource) + VideoManager::instance->captureManager()->captureTaskTokenSource->cancel(); + VideoManager::instance->captureManager()->videoFrameCopyInvoker->Stop(); + })); + }) + }; + registerVideoHandlers(outgoingVideoHandlers); - }), - DRing::exportable_callback<DRing::ConfigurationSignal::RegisteredNameFound>( - [this](const std::string &accountId, int status, const std::string &address, const std::string &name) { - MSG_("<RegisteredNameFound>" + name + " : " + address); - }) - }; - registerConfHandlers(nameRegistrationHandlers); + nameRegistrationHandlers = + { + DRing::exportable_callback<DRing::ConfigurationSignal::NameRegistrationEnded>( + [this](const std::string &accountId, int status, const std::string &name) { + MSG_("\n<NameRegistrationEnded>\n"); + + }), + DRing::exportable_callback<DRing::ConfigurationSignal::RegisteredNameFound>( + [this](const std::string &accountId, int status, const std::string &address, const std::string &name) { + MSG_("<RegisteredNameFound>" + name + " : " + address); + }) + }; + registerConfHandlers(nameRegistrationHandlers); +} - gnutls_global_init(); +void +RingD::initDaemon(int flags) +{ + DRing::init(static_cast<DRing::InitFlag>(flags)); +} - DRing::init(static_cast<DRing::InitFlag>(DRing::DRING_FLAG_CONSOLE_LOG | - DRing::DRING_FLAG_DEBUG)); +void +RingD::startDaemon() +{ + if (daemonRunning_) { + ERR_("daemon already runnging"); + return; + } + //eraseCacheFolder(); + editModeOn_ = true; + IAsyncAction^ action = ThreadPool::RunAsync(ref new WorkItemHandler([=](IAsyncAction^ spAction) + { daemonRunning_ = DRing::start(); + auto vcm = Video::VideoManager::instance->captureManager(); + std::string deviceName = DRing::getDefaultDevice(); + std::map<std::string, std::string> settings = DRing::getSettings(deviceName); + int rate = stoi(settings["rate"]); + std::string size = settings["size"]; + std::string::size_type pos = size.find('x'); + int width = std::stoi(size.substr(0, pos)); + int height = std::stoi(size.substr(pos + 1, size.length())); + for (auto dev : vcm->deviceList) { + if (!Utils::toString(dev->name()).compare(deviceName)) + vcm->activeDevice = dev; + } + vcm->activeDevice->SetDeviceProperties("", width, height, rate); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, + ref new DispatchedHandler([=]() { + finishCaptureDeviceEnumeration(); + })); + if (!daemonRunning_) { ERR_("\ndaemon didn't start.\n"); return; @@ -712,17 +732,13 @@ RingClientUWP::RingD::startDaemon() } }); - - while (daemonRunning) { + while (daemonRunning_) { DRing::pollEvents(); dequeueTasks(); Sleep(5); } - DRing::fini(); - - gnutls_global_deinit(); } - }); + },Platform::CallbackContext::Any), WorkItemPriority::High); } RingD::RingD() diff --git a/RingD.h b/RingD.h index f3f52ac..35f6956 100644 --- a/RingD.h +++ b/RingD.h @@ -16,6 +16,7 @@ * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * **************************************************************************/ +#include <dring.h> using namespace concurrency; @@ -37,7 +38,10 @@ delegate void ExportOnRingEnded(String^ accountId, String^ pin); delegate void SummonWizard(); delegate void AccountUpdated(Account^ account); delegate void IncomingVideoMuted(String^ callId, bool state); +delegate void FinishCaptureDeviceEnumeration(); +using SharedCallback = std::shared_ptr<DRing::CallbackWrapperBase>; +using namespace std::placeholders; public ref class RingD sealed { @@ -77,6 +81,8 @@ internal: // why this property has to be internal and not public ? internal: /* functions */ + void registerCallbacks(); + void initDaemon(int flags); void startDaemon(); void reloadAccountList(); void sendAccountTextMessage(String^ message); @@ -119,6 +125,7 @@ internal: event SummonWizard^ summonWizard; event AccountUpdated^ accountUpdated; event IncomingVideoMuted^ incomingVideoMuted; + event FinishCaptureDeviceEnumeration^ finishCaptureDeviceEnumeration; private: /* sub classes */ @@ -181,11 +188,20 @@ private: // CallStatus translateCallStatus(String^ state); /* members */ + Windows::UI::Core::CoreDispatcher^ dispatcher; + std::string localFolder_; bool daemonRunning_ = false; std::queue<Task^> tasksList_; StartingStatus startingStatus_ = StartingStatus::NORMAL; bool editModeOn_ = false; bool debugModeOn_ = true; + + std::map<std::string, SharedCallback> callHandlers; + std::map<std::string, SharedCallback> getAppPathHandler; + std::map<std::string, SharedCallback> getAppUserNameHandler; + std::map<std::string, SharedCallback> incomingVideoHandlers; + std::map<std::string, SharedCallback> outgoingVideoHandlers; + std::map<std::string, SharedCallback> nameRegistrationHandlers; }; } \ No newline at end of file diff --git a/RingDebug.cpp b/RingDebug.cpp index 8835c79..732c5b5 100644 --- a/RingDebug.cpp +++ b/RingDebug.cpp @@ -20,6 +20,8 @@ /* client */ #include "pch.h" +#include "fileutils.h" + using namespace RingClientUWP; using namespace Platform; @@ -56,14 +58,15 @@ RingDebug::print(const std::string& message, /* fire the event. */ auto line = ref new String(wString.c_str(), wString.length()); messageToScreen(line); - FileIO::AppendTextAsync(_logFile, line + "\n"); + + std::ofstream ofs; + ofs.open ("debug.log", std::ofstream::out | std::ofstream::app); + ofs << Utils::toString(line) << "\n"; + ofs.close(); } void RingClientUWP::RingDebug::WriteLine(String^ str) { - /* save in file */ - //FileIO::AppendTextAsync(_videoFile, str + "\n"); - /* screen in visual studio console */ std::wstringstream wStringstream; wStringstream << str->Data() << "\n"; @@ -72,19 +75,4 @@ void RingClientUWP::RingDebug::WriteLine(String^ str) RingClientUWP::RingDebug::RingDebug() { - StorageFolder^ storageFolder = ApplicationData::Current->LocalFolder; - - StorageFile^ logFile; - - task<StorageFile^>(storageFolder->CreateFileAsync("debug.log", CreationCollisionOption::ReplaceExisting)).then([this](StorageFile^ file) - { - this->_logFile = file; - }); - - task<StorageFile^>(storageFolder->CreateFileAsync("video.log", CreationCollisionOption::ReplaceExisting)).then([this](StorageFile^ file) - { - this->_videoFile = file; - }); - -} - +} \ No newline at end of file diff --git a/RingDebug.h b/RingDebug.h index 20467ba..f9ca4b4 100644 --- a/RingDebug.h +++ b/RingDebug.h @@ -42,9 +42,6 @@ public: } } - property StorageFile^ _logFile; - property StorageFile^ _videoFile; - /* properties */ /* functions */ diff --git a/SmartPanel.xaml b/SmartPanel.xaml index 95d8ce8..3d0492e 100644 --- a/SmartPanel.xaml +++ b/SmartPanel.xaml @@ -880,7 +880,30 @@ <Grid x:Name="_settings_" Grid.Row="0" Visibility="Collapsed"> - <TextBlock>some settings</TextBlock> + <Grid.RowDefinitions> + <RowDefinition Height="*"/> + </Grid.RowDefinitions> + <Grid x:Name="_videoSettings_" + Grid.Row="0"> + <StackPanel Margin="10"> + <TextBlock Text="Video Device" + Margin="10"/> + <ComboBox x:Name="_videoDeviceComboBox_" + Margin="10" + SelectionChanged="_videoDeviceComboBox__SelectionChanged"> + </ComboBox> + <TextBlock Text="Video Resolution" Margin="10"/> + <ComboBox x:Name="_videoResolutionComboBox_" + Margin="10" + SelectionChanged="_videoResolutionComboBox__SelectionChanged"> + </ComboBox> + <TextBlock Text="Video Rate" Margin="10"/> + <ComboBox x:Name="_videoRateComboBox_" + Margin="10" + SelectionChanged="_videoRateComboBox__SelectionChanged"> + </ComboBox> + </StackPanel> + </Grid> </Grid> </Grid> </Grid> diff --git a/SmartPanel.xaml.cpp b/SmartPanel.xaml.cpp index 29ede5a..a72d332 100644 --- a/SmartPanel.xaml.cpp +++ b/SmartPanel.xaml.cpp @@ -29,6 +29,7 @@ using namespace RingClientUWP::Controls; using namespace RingClientUWP::Views; using namespace RingClientUWP::ViewModel; using namespace Windows::Media::Capture; +using namespace Windows::Media::MediaProperties; using namespace Windows::UI::Xaml; using namespace Windows::Storage; using namespace Windows::UI::Xaml::Media::Imaging; @@ -41,8 +42,6 @@ using namespace Windows::Foundation; using namespace Concurrency; using namespace Platform::Collections; - - using namespace Windows::ApplicationModel::Core; using namespace Windows::Storage; using namespace Windows::UI::Core; @@ -141,7 +140,9 @@ SmartPanel::SmartPanel() RingD::instance->exportOnRingEnded += ref new RingClientUWP::ExportOnRingEnded(this, &RingClientUWP::Views::SmartPanel::OnexportOnRingEnded); RingD::instance->accountUpdated += ref new RingClientUWP::AccountUpdated(this, &RingClientUWP::Views::SmartPanel::OnaccountUpdated); - + RingD::instance->finishCaptureDeviceEnumeration += ref new RingClientUWP::FinishCaptureDeviceEnumeration([this]() { + populateVideoDeviceSettingsComboBox(); + }); } void @@ -200,12 +201,30 @@ void RingClientUWP::Views::SmartPanel::_settings__Checked(Object^ sender, Routed { _smartGrid_->Visibility = Windows::UI::Xaml::Visibility::Collapsed; _settings_->Visibility = Windows::UI::Xaml::Visibility::Visible; + auto vcm = Video::VideoManager::instance->captureManager(); + if (!vcm->isInitialized) + vcm->InitializeCameraAsync(true); + else + vcm->StartPreviewAsync(true); + summonPreviewPage(); } void RingClientUWP::Views::SmartPanel::_settings__Unchecked(Object^ sender, RoutedEventArgs^ e) { _settings_->Visibility = Windows::UI::Xaml::Visibility::Collapsed; _smartGrid_->Visibility = Windows::UI::Xaml::Visibility::Visible; + Video::VideoManager::instance->captureManager()->StopPreviewAsync() + .then([](task<void> stopPreviewTask) + { + try { + stopPreviewTask.get(); + Video::VideoManager::instance->captureManager()->isSettingsPreviewing = false; + } + catch (Exception^ e) { + WriteException(e); + } + }); + hidePreviewPage(); } void RingClientUWP::Views::SmartPanel::setMode(RingClientUWP::Views::SmartPanel::Mode mode) @@ -888,11 +907,10 @@ void RingClientUWP::Views::SmartPanel::_selectedAccountAvatarContainer__PointerR .then([this](StorageFile^ photoFile) { if (photoFile != nullptr) { - // maybe it would be possible to move some logics to the style sheet auto brush = ref new ImageBrush(); auto circle = ref new Ellipse(); - circle->Height = 80; // TODO : use some global constant when ready + circle->Height = 80; circle->Width = 80; auto path = photoFile->Path; auto uri = ref new Windows::Foundation::Uri(path); @@ -950,3 +968,147 @@ void RingClientUWP::Views::SmartPanel::Grid_PointerMoved(Platform::Object^ sende item->_hovered = Windows::UI::Xaml::Visibility::Visible; } + +// VIDEO + +void +SmartPanel::_videoDeviceComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^) +{ + if (_videoDeviceComboBox_->Items->Size) { + Video::VideoCaptureManager^ vcm = Video::VideoManager::instance->captureManager(); + auto selectedItem = static_cast<ComboBoxItem^>(static_cast<ComboBox^>(sender)->SelectedItem); + auto deviceId = static_cast<String^>(selectedItem->Tag); + std::vector<int>::size_type index; + for(index = 0; index != vcm->deviceList->Size; index++) { + auto dev = vcm->deviceList->GetAt(index); + if (dev->id() == deviceId) { + break; + } + } + vcm->activeDevice = vcm->deviceList->GetAt(index); + + populateVideoResolutionSettingsComboBox(); + } +} + +void +SmartPanel::_videoResolutionComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^) +{ + if (_videoResolutionComboBox_->Items->Size) { + Video::VideoCaptureManager^ vcm = Video::VideoManager::instance->captureManager(); + auto device = vcm->activeDevice; + auto selectedItem = static_cast<ComboBoxItem^>(static_cast<ComboBox^>(sender)->SelectedItem); + auto selectedResolution = static_cast<String^>(selectedItem->Tag); + std::vector<int>::size_type index; + for(index = 0; index != device->resolutionList()->Size; index++) { + auto res = device->resolutionList()->GetAt(index); + if (res->getFriendlyName() == selectedResolution) { + break; + } + } + vcm->activeDevice->setCurrentResolution( device->resolutionList()->GetAt(index) ); + populateVideoRateSettingsComboBox(); + } +} + +void +SmartPanel::_videoRateComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^) +{ + if (_videoRateComboBox_->Items->Size) { + Video::VideoCaptureManager^ vcm = Video::VideoManager::instance->captureManager(); + auto resolution = vcm->activeDevice->currentResolution(); + auto selectedItem = static_cast<ComboBoxItem^>(static_cast<ComboBox^>(sender)->SelectedItem); + unsigned int frame_rate = static_cast<unsigned int>(selectedItem->Tag); + std::vector<int>::size_type index; + for(index = 0; index != resolution->rateList()->Size; index++) { + auto rate = resolution->rateList()->GetAt(index); + if (rate->value() == frame_rate) + break; + } + vcm->activeDevice->currentResolution()->setActiveRate( resolution->rateList()->GetAt(index) ); + if (vcm->isPreviewing) { + vcm->CleanupCameraAsync() + .then([=](task<void> cleanupCameraTask) { + try { + cleanupCameraTask.get(); + vcm->InitializeCameraAsync(true); + } + catch (Exception^ e) { + WriteException(e); + } + }); + } + } +} + +void +SmartPanel::populateVideoDeviceSettingsComboBox() +{ + _videoDeviceComboBox_->Items->Clear(); + auto devices = Video::VideoManager::instance->captureManager()->deviceList; + int index = 0; + bool deviceSelected = false; + for (auto device : devices) { + ComboBoxItem^ comboBoxItem = ref new ComboBoxItem(); + comboBoxItem->Content = device->name(); + comboBoxItem->Tag = device->id(); + _videoDeviceComboBox_->Items->Append(comboBoxItem); + if (device->isActive() && !deviceSelected) { + _videoDeviceComboBox_->SelectedIndex = index; + deviceSelected = true; + } + ++index; + } + if (!deviceSelected && devices->Size > 0) + _videoDeviceComboBox_->SelectedIndex = 0; +} + +void +SmartPanel::populateVideoResolutionSettingsComboBox() +{ + _videoResolutionComboBox_->Items->Clear(); + Video::Device^ device = Video::VideoManager::instance->captureManager()->activeDevice; + std::map<std::string, std::string> settings = DRing::getSettings(Utils::toString(device->name())); + std::string preferredResolution = settings[Video::Device::PreferenceNames::SIZE]; + auto resolutions = device->resolutionList(); + int index = 0; + bool resolutionSelected = false; + for (auto resolution : resolutions) { + ComboBoxItem^ comboBoxItem = ref new ComboBoxItem(); + comboBoxItem->Content = resolution->getFriendlyName(); + comboBoxItem->Tag = resolution->getFriendlyName(); + _videoResolutionComboBox_->Items->Append(comboBoxItem); + if (!preferredResolution.compare(Utils::toString(resolution->getFriendlyName())) && !resolutionSelected) { + _videoResolutionComboBox_->SelectedIndex = index; + resolutionSelected = true; + } + ++index; + } + if (!resolutionSelected && resolutions->Size > 0) + _videoResolutionComboBox_->SelectedIndex = 0; +} + +void +SmartPanel::populateVideoRateSettingsComboBox() +{ + _videoRateComboBox_->Items->Clear(); + Video::Device^ device = Video::VideoManager::instance->captureManager()->activeDevice; + std::map<std::string, std::string> settings = DRing::getSettings(Utils::toString(device->name())); + std::string preferredRate = settings[Video::Device::PreferenceNames::RATE]; + auto resolution = device->currentResolution(); + int index = 0; + bool rateSelected = false; + for (auto rate : resolution->rateList()) { + ComboBoxItem^ comboBoxItem = ref new ComboBoxItem(); + comboBoxItem->Content = rate->name(); + comboBoxItem->Tag = rate->value(); + _videoRateComboBox_->Items->Append(comboBoxItem); + if (std::stoi(preferredRate) == rate->value() && !rateSelected) { + _videoRateComboBox_->SelectedIndex = index; + rateSelected = true; + } + ++index; + } + if (!rateSelected && resolution->rateList()->Size > 0) + _videoRateComboBox_->SelectedIndex = 0; +} \ No newline at end of file diff --git a/SmartPanel.xaml.h b/SmartPanel.xaml.h index cc53de7..6c62368 100644 --- a/SmartPanel.xaml.h +++ b/SmartPanel.xaml.h @@ -25,6 +25,8 @@ delegate void ToggleSmartPan(); delegate void SummonMessageTextPage(); delegate void SummonVideoPage(); delegate void SummonWelcomePage(); +delegate void SummonPreviewPage(); +delegate void HidePreviewPage(); namespace Views { @@ -91,6 +93,8 @@ internal: event SummonMessageTextPage^ summonMessageTextPage; event SummonVideoPage^ summonVideoPage; event SummonWelcomePage^ summonWelcomePage; + event SummonPreviewPage^ summonPreviewPage; + event HidePreviewPage^ hidePreviewPage; void setMode(RingClientUWP::Views::SmartPanel::Mode mode); private: @@ -116,6 +120,12 @@ private: void Grid_PointerExited(Platform::Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e); void _contactItem__PointerReleased(Platform::Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e); void generateQRcode(); + void _videoDeviceComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^); + void _videoResolutionComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^); + void _videoRateComboBox__SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^); + void populateVideoDeviceSettingsComboBox(); + void populateVideoResolutionSettingsComboBox(); + void populateVideoRateSettingsComboBox(); /* members */ void _devicesMenuButton__Unchecked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); diff --git a/Utils.h b/Utils.h index aacef25..5739619 100644 --- a/Utils.h +++ b/Utils.h @@ -32,12 +32,11 @@ namespace RingClientUWP { namespace Utils { - task<bool> fileExists(StorageFolder^ folder, String^ fileName) { return create_task(folder->GetFileAsync(fileName)) - .then([](task<StorageFile^> taskResult) + .then([](task<StorageFile^> taskResult) { bool exists; try { @@ -176,82 +175,91 @@ TrimCmd(Platform::String^ s) const WCHAR* first = s->Begin(); const WCHAR* last = s->End(); - while (first != last && last[0] != '\ ' ) - --last; + while (first != last && last[0] != '\ ') + --last; - //last--; + //last--; - return ref new Platform::String(first, sizeof(last)); - } + return ref new Platform::String(first, sizeof(last)); +} - Platform::String^ - TrimParameter(Platform::String^ s) - { - const WCHAR* first = s->Begin(); - const WCHAR* last = s->End(); +Platform::String^ +TrimParameter(Platform::String^ s) +{ + const WCHAR* first = s->Begin(); + const WCHAR* last = s->End(); - while (first != last && *first != '[') - ++first; + while (first != last && *first != '[') + ++first; - while (first != last && last[-1] != ']') - --last; + while (first != last && last[-1] != ']') + --last; - first++; - last--; + first++; + last--; - if (static_cast<unsigned int>(last - first) > 0) - return ref new Platform::String(first, static_cast<unsigned int>(last - first)); - else - return ""; - } + if (static_cast<unsigned int>(last - first) > 0) + return ref new Platform::String(first, static_cast<unsigned int>(last - first)); + else + return ""; +} - Platform::String^ - GetNewGUID() - { - GUID result; - HRESULT hr = CoCreateGuid(&result); +Platform::String^ +GetNewGUID() +{ + GUID result; + HRESULT hr = CoCreateGuid(&result); - if (SUCCEEDED(hr)) { - Guid guid(result); - return guid.ToString(); - } + if (SUCCEEDED(hr)) { + Guid guid(result); + return guid.ToString(); + } - throw Exception::CreateException(hr); - } + throw Exception::CreateException(hr); +} - std::string - getStringFromFile(const std::string& filename) - { - std::ifstream file(filename); - return std::string((std::istreambuf_iterator<char>(file)), - (std::istreambuf_iterator<char>())); - } +std::string +getStringFromFile(const std::string& filename) +{ + std::ifstream file(filename); + return std::string((std::istreambuf_iterator<char>(file)), + (std::istreambuf_iterator<char>())); +} - inline Map<String^,String^>^ - convertMap(const std::map<std::string, std::string>& m) - { - auto temp = ref new Map<String^,String^>; - for (const auto& pair : m) { - temp->Insert( +inline Map<String^, String^>^ +convertMap(const std::map<std::string, std::string>& m) +{ + auto temp = ref new Map<String^, String^>; + for (const auto& pair : m) { + temp->Insert( Utils::toPlatformString(pair.first), Utils::toPlatformString(pair.second) - ); - } - return temp; - } + ); + } + return temp; +} - inline std::map<std::string, std::string> - convertMap(Map<String^,String^>^ m) - { - std::map<std::string, std::string> temp; - for (const auto& pair : m) { - temp.insert( +inline std::map<std::string, std::string> +convertMap(Map<String^, String^>^ m) +{ + std::map<std::string, std::string> temp; + for (const auto& pair : m) { + temp.insert( std::make_pair( - Utils::toString(pair->Key), - Utils::toString(pair->Value))); - } - return temp; - } + Utils::toString(pair->Key), + Utils::toString(pair->Value))); + } + return temp; +} - } - } +template<class T> +bool +findIn(std::vector<T> vec, T val) +{ + if (std::find(vec.begin(), vec.end(), val) == vec.end()) + return true; + return false; +} + +} /*namespace Utils*/ +} /*namespace RingClientUWP*/ \ No newline at end of file diff --git a/Video.cpp b/Video.cpp index e9e71a0..2a178d6 100644 --- a/Video.cpp +++ b/Video.cpp @@ -25,43 +25,18 @@ using namespace RingClientUWP; using namespace Video; using namespace Platform; +using namespace Windows::Media::MediaProperties; /************************************************************ * * - * Size * + * Rate * * * ***********************************************************/ - -unsigned int -Video::Size::width() -{ - return m_Width; -} - -unsigned int -Video::Size::height() -{ - return m_Height; -} - -void -Video::Size::setWidth(unsigned int width) +Rate::Rate() { - m_Width = width; + m_validFormats = ref new Vector<String^>(); } -void -Video::Size::setHeight(unsigned int height) -{ - m_Height = height; -} - -/************************************************************ - * * - * Rate * - * * - ***********************************************************/ - String^ Rate::name() { @@ -74,56 +49,46 @@ Rate::value() return m_value; } -void -Rate::setName(String^ name) -{ - m_name = name; -} - -void -Rate::setValue(unsigned int value) +String^ +Rate::format() { - m_value = value; + return m_currentFormat; } -/************************************************************ - * * - * Channel * - * * - ***********************************************************/ -Channel::Channel() +Vector<String^>^ +Rate::formatList() { - m_validResolutions = ref new Vector<Resolution^>(); + return m_validFormats; } -String^ -Channel::name() +IMediaEncodingProperties^ +Rate::getMediaEncodingProperties() { - return m_name; + return m_encodingProperties; } -Resolution^ -Channel::currentResolution() +void +Rate::setName(String^ name) { - return m_currentResolution; + m_name = name; } void -Channel::setName(String^ name) +Rate::setValue(unsigned int value) { - m_name = name; + m_value = value; } void -Channel::setCurrentResolution(Resolution^ currentResolution) +Rate::setFormat(String^ format) { - m_currentResolution = currentResolution; + m_currentFormat = format; } -Vector<Resolution^>^ -Channel::resolutionList() +void +Rate::setMediaEncodingProperties(IMediaEncodingProperties^ props) { - return m_validResolutions; + m_encodingProperties = props; } /************************************************************ @@ -132,20 +97,33 @@ Channel::resolutionList() * * ***********************************************************/ -Resolution::Resolution() +Resolution::Resolution(IMediaEncodingProperties^ encodingProperties): + m_encodingProperties(encodingProperties) { - m_size = ref new Size(); m_validRates = ref new Vector<Rate^>(); + VideoEncodingProperties^ vidprops = static_cast<VideoEncodingProperties^>(encodingProperties); + m_width = vidprops->Width; + m_height = vidprops->Height; } -Resolution::Resolution(Video::Size^ size): - m_size(size) -{ } +String^ +Resolution::getFriendlyName() +{ + std::wstringstream ss; + ss << m_width << "x" << m_height; + return ref new String(ss.str().c_str()); +} + +IMediaEncodingProperties^ +Resolution::getMediaEncodingProperties() +{ + return m_encodingProperties; +} String^ Resolution::name() { - return size()->width().ToString() + "x" + size()->height().ToString(); + return m_width.ToString() + "x" + m_height.ToString(); } Rate^ @@ -160,28 +138,34 @@ Resolution::rateList() return m_validRates; } -Video::Size^ -Resolution::size() +unsigned int +Resolution::width() { - return m_size; + return m_width; } -String^ -Resolution::format() +unsigned int +Resolution::height() { - return m_format; + return m_height; } void -Resolution::setWidth(int width) +Resolution::setWidth(unsigned int width) { - m_size->setWidth(width); + m_width = width; } void -Resolution::setHeight(int height) +Resolution::setHeight(unsigned int height) { - m_size->setHeight(height); + m_height = height; +} + +String^ +Resolution::format() +{ + return m_format; } void @@ -197,10 +181,15 @@ Resolution::setActiveRate(Rate^ rate) return false; m_currentRate = rate; - // set camera device rate here return true; } +void +Resolution::setMediaEncodingProperties(IMediaEncodingProperties^ props) +{ + m_encodingProperties = props; +} + /************************************************************ * * * Device * @@ -210,7 +199,7 @@ Resolution::setActiveRate(Rate^ rate) Device::Device(String^ id) { m_deviceId = id; - m_channels = ref new Vector<Channel^>(); + m_validResolutions = ref new Vector<Resolution^>(); } String^ @@ -219,31 +208,22 @@ Device::id() return m_deviceId; } -Vector<Channel^>^ -Device::channelList() -{ - return m_channels; -} - String^ Device::name() { return m_name; } -Channel^ -Device::channel() +Resolution^ +Device::currentResolution() { - return m_currentChannel; + return m_currentResolution; } -bool -Device::setCurrentChannel(Channel^ channel) +Vector<Resolution^>^ +Device::resolutionList() { - if (m_currentChannel == channel) - return false; - m_currentChannel = channel; - return true; + return m_validResolutions; } void @@ -252,6 +232,12 @@ Device::setName(String^ name) m_name = name; } +void +Device::setCurrentResolution(Resolution^ currentResolution) +{ + m_currentResolution = currentResolution; +} + void Device::save() { @@ -260,22 +246,22 @@ Device::save() bool Device::isActive() { - return false; - //return Video::DeviceModel::instance().activeDevice() == this; + return Video::VideoManager::instance->captureManager()->activeDevice == this; } void Device::SetDeviceProperties(String^ format, int width, int height, int rate) { - auto rl = m_currentChannel->resolutionList(); - for (auto res : rl) { - if (res->format() == format && - res->size()->width() == width && - res->size()->height() == height && - res->activeRate()->value() == rate) + for (auto resolution_ : m_validResolutions) { + if ( resolution_->width() == width && + resolution_->height() == height ) { - m_currentChannel->setCurrentResolution(res); - RingDebug::instance->WriteLine("SetDeviceProperties"); + setCurrentResolution(resolution_); + for (auto rate_ : resolution_->rateList()) { + if (rate_->value() == rate && + (format->IsEmpty()? true : rate_->format() == format)) + currentResolution()->setActiveRate(rate_); + } return; } } diff --git a/Video.h b/Video.h index ad544d5..ef1db3a 100644 --- a/Video.h +++ b/Video.h @@ -20,6 +20,7 @@ #pragma once using namespace Platform; +using namespace Windows::Media::MediaProperties; namespace RingClientUWP { @@ -31,87 +32,61 @@ ref class Rate; ref class Resolution; ref class Device; -public ref class Size sealed -{ -internal: - unsigned int width (); - unsigned int height (); - - void setWidth ( unsigned int width ); - void setHeight ( unsigned int height ); - -public: - Size() { }; - Size(unsigned int w, unsigned int h): - m_Width(w), - m_Height(h) { }; - Size(Size^ rhs): - m_Width(rhs->m_Width), - m_Height(rhs->m_Height) { }; - -private: - unsigned int m_Width; - unsigned int m_Height; - -}; - public ref class Rate sealed { internal: - String^ name (); - unsigned int value (); + String^ name (); + unsigned int value (); + Vector<String^>^ formatList (); + String^ format (); + IMediaEncodingProperties^ getMediaEncodingProperties (); - void setName ( String^ name ); - void setValue ( unsigned int value ); + void setName ( String^ name ); + void setValue ( unsigned int value ); + void setFormat ( String^ format ); -private: - String^ m_name; - unsigned int m_value; - -}; - -public ref class Channel sealed -{ -internal: - String^ name (); - Resolution^ currentResolution (); - Vector<Resolution^>^ resolutionList (); - - void setName ( String^ name ); - void setCurrentResolution ( Resolution^ currentResolution); + void setMediaEncodingProperties (IMediaEncodingProperties^ props); public: - Channel(); + Rate(); private: + IMediaEncodingProperties^ m_encodingProperties; String^ m_name; - Resolution^ m_currentResolution; - Vector<Resolution^>^ m_validResolutions; + unsigned int m_value; + String^ m_currentFormat; + Vector<String^>^ m_validFormats; }; public ref class Resolution sealed { internal: - String^ name (); - Rate^ activeRate (); - Vector<Rate^>^ rateList (); - Size^ size (); - String^ format (); - - bool setActiveRate ( Rate^ rate ); - void setWidth ( int width ); - void setHeight ( int height ); - void setFormat ( String^ format ); + String^ name (); + Rate^ activeRate (); + Vector<Rate^>^ rateList (); + unsigned int width (); + unsigned int height (); + String^ format (); + String^ getFriendlyName (); + IMediaEncodingProperties^ getMediaEncodingProperties (); + + bool setActiveRate ( Rate^ rate ); + void setWidth ( unsigned int width ); + void setHeight ( unsigned int height ); + void setFormat ( String^ format ); + + void setMediaEncodingProperties (IMediaEncodingProperties^ props); public: - Resolution(); - Resolution(Size^ size); + Resolution(IMediaEncodingProperties^ encodingProperties); private: + IMediaEncodingProperties^ m_encodingProperties; Rate^ m_currentRate; Vector<Rate^>^ m_validRates; - Size^ m_size; + unsigned int m_width; + unsigned int m_height; String^ m_format; }; @@ -123,18 +98,16 @@ internal: public: constexpr static const char* RATE = "rate" ; constexpr static const char* NAME = "name" ; - constexpr static const char* CHANNEL = "channel"; constexpr static const char* SIZE = "size" ; }; - Vector<Channel^>^ channelList (); - String^ id (); - String^ name (); - Channel^ channel (); + String^ id (); + String^ name (); + Resolution^ currentResolution (); + Vector<Resolution^>^ resolutionList (); - bool setCurrentChannel ( Channel^ channel ); - void setName ( String^ name ); - + void setName ( String^ name ); + void setCurrentResolution ( Resolution^ currentResolution ); public: Device(String^ id); @@ -144,11 +117,11 @@ public: bool isActive (); private: - String^ m_deviceId ; - String^ m_name ; - Channel^ m_currentChannel ; - Vector<Channel^>^ m_channels ; - bool m_requireSave ; + String^ m_deviceId ; + String^ m_name ; + bool m_requireSave ; + Resolution^ m_currentResolution ; + Vector<Resolution^>^ m_validResolutions ; }; diff --git a/VideoCaptureManager.cpp b/VideoCaptureManager.cpp index 5f7d874..3c50e26 100644 --- a/VideoCaptureManager.cpp +++ b/VideoCaptureManager.cpp @@ -19,12 +19,15 @@ #include "pch.h" #include "VideoCaptureManager.h" +#include "SmartPanel.xaml.h" #include <MemoryBuffer.h> // IMemoryBufferByteAccess using namespace RingClientUWP; +using namespace RingClientUWP::Views; using namespace Video; +using namespace Windows::UI::Core; using namespace Windows::Graphics::Display; using namespace Windows::Graphics::Imaging; using namespace Windows::UI::Xaml::Media::Imaging; @@ -32,10 +35,18 @@ using namespace Windows::Media; using namespace Windows::Media::MediaProperties; using namespace Windows::Media::Capture; +std::map<std::string, int> pixel_formats = { + {"NV12", 0}, + {"MJPG", 1}, + {"RGB24",2}, + {"YUV2", 3} +}; + VideoCaptureManager::VideoCaptureManager(): mediaCapture(nullptr) , isInitialized(false) , isPreviewing_(false) + , isSettingsPreviewing_(false) , isChangingCamera(false) , isRendering(false) , externalCamera(false) @@ -60,13 +71,13 @@ VideoCaptureManager::getSettings(String^ device) void VideoCaptureManager::MediaCapture_Failed(Capture::MediaCapture^, Capture::MediaCaptureFailedEventArgs^ errorEventArgs) { - RingDebug::instance->WriteLine("MediaCapture_Failed"); std::wstringstream ss; ss << "MediaCapture_Failed: 0x" << errorEventArgs->Code << ": " << errorEventArgs->Message->Data(); RingDebug::instance->WriteLine(ref new String(ss.str().c_str())); if (captureTaskTokenSource) captureTaskTokenSource->cancel(); + CleanupCameraAsync(); } @@ -84,7 +95,6 @@ VideoCaptureManager::CleanupCameraAsync() auto stopPreviewTask = create_task(StopPreviewAsync()); taskList.push_back(stopPreviewTask); } - isInitialized = false; } @@ -99,51 +109,30 @@ VideoCaptureManager::CleanupCameraAsync() }); } -task<void> -VideoCaptureManager::EnumerateWebcamsAsync() -{ - devInfoCollection = nullptr; - - deviceList->Clear(); - - return create_task(DeviceInformation::FindAllAsync(DeviceClass::VideoCapture)) - .then([this](task<DeviceInformationCollection^> findTask) - { - try { - devInfoCollection = findTask.get(); - if (devInfoCollection == nullptr || devInfoCollection->Size == 0) { - RingDebug::instance->WriteLine("No WebCams found."); - } - else { - for (unsigned int i = 0; i < devInfoCollection->Size; i++) { - AddVideoDevice(i); - } - RingDebug::instance->WriteLine("Enumerating Webcams completed successfully."); - } - } - catch (Platform::Exception^ e) { - WriteException(e); - } - }); -} task<void> -VideoCaptureManager::StartPreviewAsync() +VideoCaptureManager::StartPreviewAsync(bool isSettingsPreview) { - RingDebug::instance->RingDebug::instance->WriteLine("StartPreviewAsync"); + WriteLine("StartPreviewAsync"); displayRequest->RequestActive(); - auto sink = getSink(); + Windows::UI::Xaml::Controls::CaptureElement^ sink; + if (isSettingsPreview) + sink = getSettingsPreviewSink(); + else + sink = getSink(); sink->Source = mediaCapture.Get(); return create_task(mediaCapture->StartPreviewAsync()) - .then([this](task<void> previewTask) + .then([=](task<void> previewTask) { try { previewTask.get(); isPreviewing = true; + if (isSettingsPreview) + isSettingsPreviewing = true; startPreviewing(); - RingDebug::instance->WriteLine("StartPreviewAsync DONE"); + WriteLine("StartPreviewAsync DONE"); } catch (Exception ^e) { WriteException(e); @@ -181,7 +170,7 @@ VideoCaptureManager::StopPreviewAsync() } task<void> -VideoCaptureManager::InitializeCameraAsync() +VideoCaptureManager::InitializeCameraAsync(bool isSettingsPreview) { RingDebug::instance->WriteLine("InitializeCameraAsync"); @@ -190,26 +179,21 @@ VideoCaptureManager::InitializeCameraAsync() mediaCapture = ref new MediaCapture(); - auto devInfo = devInfoCollection->GetAt(0); //preferences - video capture device - mediaCaptureFailedEventToken = mediaCapture->Failed += - ref new Capture::MediaCaptureFailedEventHandler(this, &VideoCaptureManager::MediaCapture_Failed); - - if (devInfo == nullptr) - return create_task([]() {}); + ref new Capture::MediaCaptureFailedEventHandler(this, &VideoCaptureManager::MediaCapture_Failed); auto settings = ref new MediaCaptureInitializationSettings(); - settings->VideoDeviceId = devInfo->Id; + settings->VideoDeviceId = activeDevice->id(); return create_task(mediaCapture->InitializeAsync(settings)) - .then([this](task<void> initTask) + .then([=](task<void> initTask) { try { initTask.get(); SetCaptureSettings(); isInitialized = true; RingDebug::instance->WriteLine("InitializeCameraAsync DONE"); - return StartPreviewAsync(); + return StartPreviewAsync(isSettingsPreview); } catch (Exception ^e) { WriteException(e); @@ -218,8 +202,51 @@ VideoCaptureManager::InitializeCameraAsync() }); } -void -VideoCaptureManager::AddVideoDevice(uint8_t index) +task<void> +VideoCaptureManager::EnumerateWebcamsAsync() +{ + devInfoCollection = nullptr; + + deviceList->Clear(); + + // TODO: device monitor + //auto watcher = DeviceInformation::CreateWatcher(); + + return create_task(DeviceInformation::FindAllAsync(DeviceClass::VideoCapture)) + .then([this](task<DeviceInformationCollection^> findTask) + { + try { + devInfoCollection = findTask.get(); + if (devInfoCollection == nullptr || devInfoCollection->Size == 0) { + WriteLine("No WebCams found."); + } + else { + std::vector<task<void>> taskList; + for (unsigned int i = 0; i < devInfoCollection->Size; i++) { + taskList.push_back(AddVideoDeviceAsync(i)); + } + when_all(taskList.begin(), taskList.end()) + .then([this](task<void> previousTasks) + { + try { + previousTasks.get(); + RingD::instance->startDaemon(); + } + catch (Exception^ e) { + ERR_("One doesn't simply start Ring daemon..."); + WriteException(e); + } + }); + } + } + catch (Platform::Exception^ e) { + WriteException(e); + } + }); +} + +task<void> +VideoCaptureManager::AddVideoDeviceAsync(uint8_t index) { RingDebug::instance->WriteLine("GetDeviceCaps " + index.ToString()); Platform::Agile<Windows::Media::Capture::MediaCapture^> mc; @@ -228,47 +255,81 @@ VideoCaptureManager::AddVideoDevice(uint8_t index) auto devInfo = devInfoCollection->GetAt(index); if (devInfo == nullptr) - return; + return concurrency::task_from_result(); auto settings = ref new MediaCaptureInitializationSettings(); settings->VideoDeviceId = devInfo->Id; - create_task(mc->InitializeAsync(settings)) + return create_task(mc->InitializeAsync(settings)) .then([=](task<void> initTask) { try { initTask.get(); auto allprops = mc->VideoDeviceController->GetAvailableMediaStreamProperties(MediaStreamType::VideoPreview); Video::Device^ device = ref new Device(devInfo->Id); - Video::Channel^ channel = ref new Channel(); for (auto props : allprops) { MediaProperties::VideoEncodingProperties^ vidprops = static_cast<VideoEncodingProperties^>(props); - int width = vidprops->Width; - int height = vidprops->Height; - Video::Resolution^ resolution = ref new Resolution(ref new Size(width,height)); + // Create resolution + Video::Resolution^ resolution = ref new Resolution(props); + // Get pixel-format + String^ format = vidprops->Subtype; + // Create rate Video::Rate^ rate = ref new Rate(); unsigned int frame_rate = 0; if (vidprops->FrameRate->Denominator != 0) frame_rate = vidprops->FrameRate->Numerator / vidprops->FrameRate->Denominator; rate->setValue(frame_rate); - rate->setName(rate->value().ToString() + "fps"); + rate->setName(rate->value().ToString() + " FPS"); + rate->setFormat(format); + // Try to find a resolution with the same dimensions in this device's resolution list + std::vector<int>::size_type resolution_index; + Video::Resolution^ matchingResolution; + for(resolution_index = 0; resolution_index != device->resolutionList()->Size; resolution_index++) { + matchingResolution = device->resolutionList()->GetAt(resolution_index); + if (matchingResolution->width() == resolution->width() && + matchingResolution->height() == resolution->height()) + break; + } + if (resolution_index < device->resolutionList()->Size) { + // Resolution found, check if rate is already in this resolution's ratelist, + // If so, pick the best format (prefer NV12 -> MJPG -> YUV2 -> RGB24 -> scrap the rest), + // otherwise append to resolution's ratelist, and continue looping + std::vector<int>::size_type rate_index; + for(rate_index = 0; rate_index != matchingResolution->rateList()->Size; rate_index++) { + auto matchingRate = matchingResolution->rateList()->GetAt(rate_index); + if (matchingRate->value() == rate->value()) + break; + } + if (rate_index < matchingResolution->rateList()->Size) { + // Rate found, pick best pixel-format + if (pixel_formats[Utils::toString(format)] < + pixel_formats[Utils::toString(matchingResolution->activeRate()->format())]) { + matchingResolution->activeRate()->setFormat(format); + // set props again + matchingResolution->activeRate()->setMediaEncodingProperties(props); + } + continue; + } + // Rate NOT found + device->resolutionList()->GetAt(resolution_index)->rateList()->Append(rate); + continue; + } + // Resolution NOT found, append rate to this resolution's ratelist and append resolution + // to device's resolutionlist + rate->setFormat(format); + rate->setMediaEncodingProperties(props); + resolution->rateList()->Append(rate); resolution->setActiveRate(rate); - String^ format = vidprops->Subtype; resolution->setFormat(format); - channel->resolutionList()->Append(resolution); - RingDebug::instance->WriteLine(devInfo->Name + " " - + width.ToString() + "x" + height.ToString() - + " " + frame_rate.ToString() + "FPS" + " " + format); + device->resolutionList()->Append(resolution); } - device->channelList()->Append(channel); - device->setCurrentChannel(device->channelList()->GetAt(0)); auto location = devInfo->EnclosureLocation; if (location != nullptr) { if (location->Panel == Windows::Devices::Enumeration::Panel::Front) { - device->setName(devInfo->Name + "-Front"); + device->setName(devInfo->Name + " - Front"); } else if (location->Panel == Windows::Devices::Enumeration::Panel::Back) { - device->setName(devInfo->Name + "-Back"); //ignore + device->setName(devInfo->Name + " - Back"); // TODO: ignore these back panel cameras..? } else { device->setName(devInfo->Name); @@ -277,9 +338,15 @@ VideoCaptureManager::AddVideoDevice(uint8_t index) else { device->setName(devInfo->Name); } + for (auto res : device->resolutionList()) { + for (auto rate : res->rateList()) { + WriteLine(device->name() + " " + res->width().ToString() + "x" + res->height().ToString() + + " " + rate->value().ToString() + "FPS " + rate->format()); + } + } this->deviceList->Append(device); this->activeDevice = deviceList->GetAt(0); - RingDebug::instance->WriteLine("GetDeviceCaps DONE"); + WriteLine("GetDeviceCaps DONE"); DRing::addVideoDevice(Utils::toString(device->name())); } catch (Platform::Exception^ e) { @@ -312,20 +379,35 @@ void VideoCaptureManager::CopyFrame(Object^ sender, Object^ e) { if (!isRendering && isPreviewing) { - try { - create_task(VideoCaptureManager::CopyFrameAsync()); - } - catch(Platform::COMException^ e) { - RingDebug::instance->WriteLine(e->ToString()); - } + create_task(VideoCaptureManager::CopyFrameAsync()) + .then([=](task<void> copyTask) + { + try { + copyTask.get(); + } + catch (Exception^ e) { + WriteException(e); + isRendering = false; + StopPreviewAsync(); + videoFrameCopyInvoker->Stop(); + if (captureTaskTokenSource) + captureTaskTokenSource->cancel(); + CleanupCameraAsync(); + throw ref new Exception(e->HResult, e->Message); + } + }); } } task<void> VideoCaptureManager::CopyFrameAsync() { - unsigned int videoFrameWidth = activeDevice->channel()->currentResolution()->size()->width(); - unsigned int videoFrameHeight = activeDevice->channel()->currentResolution()->size()->height(); + unsigned int videoFrameWidth = activeDevice->currentResolution()->width(); + unsigned int videoFrameHeight = activeDevice->currentResolution()->height(); + + auto allprops = mediaCapture->VideoDeviceController->GetAvailableMediaStreamProperties(MediaStreamType::VideoPreview); + MediaProperties::VideoEncodingProperties^ vidprops = static_cast<VideoEncodingProperties^>(allprops->GetAt(0)); + String^ format = vidprops->Subtype; // for now, only bgra auto videoFrame = ref new VideoFrame(BitmapPixelFormat::Bgra8, videoFrameWidth, videoFrameHeight); @@ -379,35 +461,53 @@ VideoCaptureManager::CopyFrameAsync() } catch (Exception^ e) { - RingDebug::instance->WriteLine("failed to copy frame to daemon's buffer"); + WriteException(e); + throw ref new Exception(e->HResult, e->Message); } - }).then([=](task<void> previousTask) { + }).then([=](task<void> renderCaptureToBufferTask) { try { - previousTask.get(); + renderCaptureToBufferTask.get(); isRendering = false; } catch (Platform::Exception^ e) { - RingDebug::instance->WriteLine( "Caught exception from previous task.\n" ); + WriteException(e); } }); } catch(Exception^ e) { WriteException(e); - throw std::exception(); + throw ref new Exception(e->HResult, e->Message); } } void VideoCaptureManager::SetCaptureSettings() { - auto vp = ref new VideoEncodingProperties; - auto res = activeDevice->channel()->currentResolution(); - vp->Width = res->size()->width(); - vp->Height = res->size()->height(); - vp->FrameRate->Numerator = res->activeRate()->value(); - vp->FrameRate->Denominator = 1; - vp->Subtype = res->format(); - auto encodingProperties = static_cast<IMediaEncodingProperties^>(vp); + WriteLine("SetCaptureSettings"); + auto res = activeDevice->currentResolution(); + auto vidprops = ref new VideoEncodingProperties; + vidprops->Width = res->width(); + vidprops->Height = res->height(); + vidprops->FrameRate->Numerator = res->activeRate()->value(); + vidprops->FrameRate->Denominator = 1; + vidprops->Subtype = res->activeRate()->format(); + auto encodingProperties = static_cast<IMediaEncodingProperties^>(vidprops); create_task(mediaCapture->VideoDeviceController->SetMediaStreamPropertiesAsync( - MediaStreamType::VideoPreview, encodingProperties)); + MediaStreamType::VideoPreview, encodingProperties)) + .then([=](task<void> setpropsTask){ + try { + setpropsTask.get(); + std::string deviceName = Utils::toString(activeDevice->name()); + std::map<std::string, std::string> settings = DRing::getSettings(deviceName); + settings["name"] = Utils::toString(activeDevice->name()); + settings["rate"] = Utils::toString(res->activeRate()->value().ToString()); + settings["size"] = Utils::toString(res->getFriendlyName()); + DRing::applySettings(deviceName, settings); + DRing::setDefaultDevice(deviceName); + WriteLine("SetCaptureSettings DONE"); + } + catch (Exception^ e) { + WriteException(e); + } + }); } \ No newline at end of file diff --git a/VideoCaptureManager.h b/VideoCaptureManager.h index 74ad03d..32bc635 100644 --- a/VideoCaptureManager.h +++ b/VideoCaptureManager.h @@ -23,6 +23,7 @@ using namespace Windows::ApplicationModel::Core; using namespace Windows::Devices::Enumeration; using namespace Windows::Media::Capture; using namespace Windows::Foundation; +using namespace Windows::UI::Xaml::Controls; using namespace Concurrency; namespace RingClientUWP @@ -30,7 +31,9 @@ namespace RingClientUWP delegate void StartPreviewing(); delegate void StopPreviewing(); -delegate Windows::UI::Xaml::Controls::CaptureElement^ GetSink(); +delegate CaptureElement^ GetSink(); +delegate CaptureElement^ GetSettingsPreviewSink(); +delegate void CaptureEnumerationComplete(); namespace Video { @@ -43,12 +46,16 @@ internal: bool get() { return isPreviewing_; } void set(bool value) { isPreviewing_ = value; } } + property bool isSettingsPreviewing + { + bool get() { return isSettingsPreviewing_; } + void set(bool value) { isSettingsPreviewing_ = value; } + } Map<String^,String^>^ getSettings(String^ device); VideoCaptureManager(); - Windows::Graphics::Display::DisplayInformation^ displayInformation; Windows::Graphics::Display::DisplayOrientations displayOrientation; Windows::System::Display::DisplayRequest^ displayRequest; @@ -68,21 +75,20 @@ internal: Platform::Agile<Windows::Media::Capture::MediaCapture^> mediaCapture; - task<void> InitializeCameraAsync(); - task<void> StartPreviewAsync(); + task<void> InitializeCameraAsync(bool isSettingsPreview); + task<void> StartPreviewAsync(bool isSettingsPreview); task<void> StopPreviewAsync(); task<void> EnumerateWebcamsAsync(); task<void> CleanupCameraAsync(); // event tokens EventRegistrationToken mediaCaptureFailedEventToken; - EventRegistrationToken displayInformationEventToken; EventRegistrationToken visibilityChangedEventToken; cancellation_token_source* captureTaskTokenSource; void MediaCapture_Failed(MediaCapture ^currentCaptureObject, MediaCaptureFailedEventArgs^ errorEventArgs); - void AddVideoDevice(uint8_t index); + task<void> AddVideoDeviceAsync(uint8_t index); void SetCaptureSettings(); DispatcherTimer^ videoFrameCopyInvoker; @@ -93,9 +99,12 @@ internal: event StartPreviewing^ startPreviewing; event StopPreviewing^ stopPreviewing; event GetSink^ getSink; + event GetSettingsPreviewSink^ getSettingsPreviewSink; + event CaptureEnumerationComplete^ captureEnumerationComplete; private: bool isPreviewing_; + bool isSettingsPreviewing_; }; diff --git a/VideoPage.xaml b/VideoPage.xaml index 79fdecc..a1cf59f 100644 --- a/VideoPage.xaml +++ b/VideoPage.xaml @@ -1,4 +1,22 @@ -<Page +<!-- ********************************************************************** +* Copyright (C) 2016 by Savoir-faire Linux * +* Author: Jäger Nicolas<nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas<andreas.traczyk@savoirfairelinux.com> * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 3 of the License, or * +* (at your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +* GNU General Public License for more details. * +* * +* You should have received a copy of the GNU General Public License * +* along with this program. If not, see <http://www.gnu.org/licenses/> . * +*********************************************************************** --> +<Page x:Class="RingClientUWP.Views.VideoPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" diff --git a/VideoPage.xaml.cpp b/VideoPage.xaml.cpp index d4be80c..959ae62 100644 --- a/VideoPage.xaml.cpp +++ b/VideoPage.xaml.cpp @@ -54,9 +54,6 @@ VideoPage::VideoPage() { InitializeComponent(); - VideoManager::instance->captureManager()->displayInformation = DisplayInformation::GetForCurrentView(); - VideoManager::instance->captureManager()->EnumerateWebcamsAsync(); - Page::NavigationCacheMode = Navigation::NavigationCacheMode::Required; VideoManager::instance->rendererManager()->writeVideoFrame += @@ -77,7 +74,7 @@ VideoPage::VideoPage() previousTask.get(); } catch (Platform::Exception^ e) { - RingDebug::instance->WriteLine( "Caught exception from previous task.\n" ); + RingDebug::instance->WriteLine( "Caught exception from WriteFrameAsSoftwareBitmapAsync task.\n" ); } }); } diff --git a/VideoPage.xaml.h b/VideoPage.xaml.h index 48c82d7..b3048d8 100644 --- a/VideoPage.xaml.h +++ b/VideoPage.xaml.h @@ -1,4 +1,3 @@ -#pragma once /************************************************************************** * Copyright (C) 2016 by Savoir-faire Linux * * Author: J�ger Nicolas <nicolas.jager@savoirfairelinux.com> * @@ -17,6 +16,8 @@ * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * **************************************************************************/ +#pragma once + #include "VideoPage.g.h" #include "MessageTextPage.xaml.h" diff --git a/WelcomePage.xaml b/WelcomePage.xaml index d8431df..b52a07f 100644 --- a/WelcomePage.xaml +++ b/WelcomePage.xaml @@ -1,6 +1,7 @@ <!-- ********************************************************************** * Copyright (C) 2016 by Savoir-faire Linux * * Author: Jäger Nicolas<nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas<andreas.traczyk@savoirfairelinux.com> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/WelcomePage.xaml.cpp b/WelcomePage.xaml.cpp index f61275d..b87451b 100644 --- a/WelcomePage.xaml.cpp +++ b/WelcomePage.xaml.cpp @@ -1,6 +1,7 @@ /************************************************************************** * Copyright (C) 2016 by Savoir-faire Linux * * Author: J�ger Nicolas <nicolas.jager@savoirfairelinux.com> * +* Author: Traczyk Andreas <andreas.traczyk@savoirfairelinux.com> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/WelcomePage.xaml.h b/WelcomePage.xaml.h index cd8a716..0e0c34f 100644 --- a/WelcomePage.xaml.h +++ b/WelcomePage.xaml.h @@ -1,4 +1,3 @@ -#pragma once /************************************************************************** * Copyright (C) 2016 by Savoir-faire Linux * * Author: J�ger Nicolas <nicolas.jager@savoirfairelinux.com> * @@ -17,6 +16,8 @@ * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * **************************************************************************/ +#pragma once + #include "WelcomePage.g.h" namespace RingClientUWP diff --git a/_language-fr.appx b/_language-fr.appx index 49546c5da3f6d3be79b44c40b259a8206fd7b285..cbd51770704c55fe85fb8207f5bce4990458eb4d 100644 GIT binary patch delta 3407 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%0Dy_NNseCKcdHL?8S z+wL~sp8STFFIhJ%Fep%~w`@&1@8YraV3QOx<C3P<;{qbKlRjiTNHKfjsMpPtGNIy5 zTMgTZO^eumarEl6dFgPK>rT4)<J-BPPq&$-|IA(e;@<b`kDTU7e);mT(?|TK<Ws5U zX_XPPBqX1RKQ((Uc{bqx`786bt$)z%eZ0{&QSAGVw~{8Nax%6@cih}?>h$~iGkx~5 z;SyGs*Ps0PD875s_l-uuALr|DcbRwBZ_nI4|MISwy!}@>e@#)f+`65Al(l`IUAtE~ zJ#zc{eP@mrEUQetu<3gE?#<z`yNVJI?O~U6Qk?lRGwAxIZLIf#^(Ic)y3hWzTV>LU zYqJ*1+`BdZ$#%|Z#dho8+?b#K$o6~d-uE>(>zCZD&l54UNw+Hczx;_gw`}LCGtRQ^ zzsstuTy^7$X74t<e(R5a@!`XdX3A)*xyM>YD}~M4^K8%KpwmB|@b_nZop1A3Zu@rK z7w_bE?kg{iD>hkj!@pp+PlT+9$mEsto<3h*>HQ+S^wgjG3<2JZOu7v4-iarqUBSY@ zU@>_MpPoFZScKI~pjt@)QZYfvn#qj(@?bUK5|xpG;UFUe1FD+h$@cv6pmKI{BEK}_ v-^sQ7N?;w}!j_eRL5GcjK@e&lgi@HikKaXbB_D(r;LXMkVm+8FET9Mg;xv=% delta 3375 zcmX@4utkA4z?+#xmw|(UgCQd-Ya*{gy?4HJsEFP5jjxwv#?_hhFZ<~0eOS4uAVyjx zj5F}EThS_$WGTgvNb|?p=~pUu7(YJJ$f~p3VUc}<=>7#s2fJ9gZzzfBM7Wmp-`MEr z^l-NI<BgYH#r7_q{dw+j+vnEl$BZ||tiD*-Yb2;_=XIp>h58$jx$~Vbq>C}l3feB+ zcDKH0-h~fU_r5DT23PHRW4rS~^@Z>9mA@u#VfnM?{eRxysal_QU&@YH6r9I%uD-PO zg=<Xwk3%1pnp*f>&Ro>%{lm6D#VJ;K$EAs_j_HC)g*J=jg-w>UrZ<;<b^2oVHaPGv z@AaGe|IdjvYiBp?PnhQus~p&>@Sj8VvCZ_e+A~5=)IZ|WWhwN%nI&`c&?LD<6?Z!4 z{HhaOyg2%XSmgt)70s*N<uCr;Ve;bmlL?M`HM$v$lR}!etnA)ly33#crErNv#4QWw z`~t0r)i>TJeE-Svntw~R#f&c{8bLEMR*Cpk@K_%6vOLpzktH{1*8b(c_OaD0of1~A z&u()v{Dw$@Tt!!X_|1}>{m0Xis^_LQY(5~o=j5gXk{k9Ni=7s1XnLgP<fl{nxPP0! zvHD<BFs=S{TBG;@_2coIET_gh|BJt|r|Y`Ff9q(5d2IVm#cz~8aJ_N)&-k3ct5*ZQ z+m{7DJbvJDr)ubThSKN4tfk)=((n5gl$<$eeEReTwUrEW9^QUoe!#T;f&bOZMa^=J z&74z~%wBa(d`7&G@HLI|`;TpDwvqZ&)1Gd}`H!((K10O9x%5nB(9?%2W<QlQ>wMGw z#jy9|e8b;NdtIYV4%Z(oj(70kP5-~7@ImMU%YWwYoEb~y8;fW8HpI@8GKq^~Y8Sn- zDtJ=3(CJq%(_Za+dgb#xmwk~ZnCc(>x+i)<^u+e7?-QmyGx*N>WqZdT?~Yxr+YdhD zi&%0YR7$o-@wA16&jl6V#3JJt8r@u#KC+S9c!R69ysW(ReMO!B>g+B0w##3?GBEdB zveJ6mov*jH+<dv_D_``Mm9PD-e|x=LcIW=jAO1xh&*Q(nWv*@ND$8HNzU51|Wc%F? zTvng{ZrbH3*1M9mLtoE0WX+ZAba?Z+f~QqmuN?04Z@uQVG<+pb&c4T|BCJhX|Ar_o z`PTX_U+(eK=UUyJ3l?SHczfN%KUH_#bT2_u*()M82Y$bJ_sf;<kwwAlqp#YteJ{7% znsU5NkG<-&LG4R1`xmSGvN`J?y!+J_xh8CCnpAtxocepZ+Do5xsJ(d8xh>@IUC}n% zfF;ivucw~x&;8wTuPEod;nR5=magoXr5aPeplY7wiUmKH?>M;dFN-{HVyRCgPwl<V z<=h#fOKsgA`^CIa*z=kB?vI!qzO2knU)Z!O;^V*ltuhpm-uLd(#rKUJygw&7PjWR9 zJUP+hTe;SxgTG5G1G|!yAHHljpL+jv@4{J*b1!yGRy(t9zs$GI3x8SJbN}P-uuFgX zpOJy#|9@tN0B?2<&3{(Q)mRw7K!F2HF`yJGlN;DXC(mP%WOD7Aypu(`p8x5s&ZsAs zq&HeyTN`^<R%c}uxxAiokyBJ>8B1v%*V5H3@5)bScy)eWz`dgPSWA|3{^j%YpNM_B zAE3zj_?GII2es9Qcb?|I(U9DGYHi`NFFNMD?b~cWvTf12SQ$4<Z^O?G{r1|iGt$)$ zT{1rJ%l@{$koC`ml+euNhbxcqn!KowI$`L+Eivm<XyYyI|4zTcoa7&7OzHIs5WDVG zuKuldb3Mm0&6{QZTZ`DYaL&!2v{IroXVV|C+L`G}Qs+18%<6sTcCo^8rtiBX#k6F{ zll|WrSbyJY|Iu)+#O!$J#DMG9PHmPdx!QVS(WRhyr`DS4My<cF^b^;<mRVJ|7e4tP z_O#xKRjuNs>=H-4mtVc?)|@i`c=*)?9n0NQ|2Qv*nSE->%;Fb!*SqICnJqkUtM2Sh z(PwrOY%3;xJ(}v|U0ryJeFy8d6^AuH86V`!X{~E?pZZuxDm4E5=g;STZ^o_um+$0z za_^=0n#Xn0l;nCJ+r6r`h_%}NL48BTYyV|`HLF<b*Bd;&?h=m_e-pQA`EO!`#M?h6 z2#p?ps`ZBqMB3i_{^9rDtFrEQ5%Xa;7uALyjb_e!Q!iaSwCh~yrjzTpCn&slYH4}i z@_hBJxwDqv>JypNd4_#i9z$<R#F~h0-wx*7+MUbfJ53{cVMxjm_L&TGAN*Q%*O&EP zo5ppK`JVC@Hl2B|IAw-V-;#5ZdMzTJE0Yf@p86~m)s}hSX+7g(4fZH`PpL_hx~@vD zTjjMrCsrieG-t+zf(5tMmE@_oUYpC^RMx-LMOt~C50}!>@W#$7pPLwUld7+7=~?Dw z;PoQ^<&7@ORdVltCU~s;dWy@MOE`Z^MP6vKUBCURXE~eiZpkt4SI&3%@#kHx;Rk<f z-@~R1Jhwg+{OLc`d`i@BQID$2v|T~Ag7aR-dF}YGQWktssQ=p+>tmDG9Z$0HYMVU$ zd)*teS5}=~$77aFKKk$F=8d-A7TZc6GWfseuVX-Dmsc{GOWY^FW0z#kh|1c`#i78+ znGu!cd8z%QZ}eni&Z+g;vC}W~YgOg`)ZOxO$~l&^e2Z(lXUu<p#dt^kcc%%9!;@Dg zw`@M0t+!Wo-|nL=t#^6kzs$X*^E-Fl!~Re9qL0?-wM*GA_rJoliqrDyZSfD^KP=z+ zU7Rid=6Sp5#eY8?n(<7a?e+34=Q7unWIrqEDfkq-LQN*)+{C?)yB2;7{@+<Yxhe1b ziM7f6Z~iLlR;o|^`s!RCtM3^lHs3Q$Y`#7AVg|;BMuwacKex=a`h9Zc(f^O{o|+)% znm@sP0pp1iygX{P&auq7X3qk|(h^0vot2&4gUyR$lhfUuV};Y*-DAC#)s<s~&4ayz z)tQ~0y@j2bEeqqEjh&g5gPo0;jg#vSYbacoIbv$WRP1TKp)-*4X5fJfsd-6H(iysn z4NVyzKR<lm{Qj<rb?$%OSv|;lyv=@Ie)awH>Dg<&uFhCq+EJ%JJ!s4J{kdP~+kVQw zGw1M5wqpO<`RCJ0&;9!{Z|gS3+p+u0*1s3NkQuXR@lNfs+=ADUr>DCIO<E*1eS>Rg z&dd5OQ2`nP-_N{w*cmgM*EqaxMrxhvBI~N18EdXtEtU56&#hF-xUli!maM<WtexbS z>=TQBbiHoU-<MC9?a5p{{f4L2**zca6VGnq&TTrLzqanSp=H>y(}l+^cUNlnALqW! z;byb*)At+GZ)I-(xxKD@<qZ43E9Tvme0}A$QGZW<O>}*s<qDVAKdyTxc};)6No(Go znaLsL|Nl>0bHMg{o&HpvxWm`J=RDg|@cDIh{nzdBpMTGMc=pJd69;xg9CkR{z%H?7 z)1jW8|B*9zBzTTE*n|r7A8+XHWuCDvGV<oknHQC2NQ$(W*6Y~$m2G>o(NZY&mZ4(G z%`dMGh0l%uQgOXz_r7|q%^`kY{XR^6(z=^h?_RXbHTBb5pL*@BtclCm+0qbKd80Kp z_hqp-hs|g2nwdHOBSSZAd$c0@<JUKO-)7r?wf_40&Huak`d-<V23J$hz4~9iDedg; zRR=e!WlwYe`)R@7=S$|zv))iQKky;n=K8H08I$Cq0?oPKTb`{AnKWbn!V~o$?Y0-a zJiB)L<zL%2%6_(3aJKr>rhpln4{hRpCna{tOsS2Z`w{>CdA7@AcjR2R`}bze-QUao zcgp8nE~`1~eN1+>nEme3v-R5l->7!Ji)&}?+hH4T|NH!7?ojvV{+*r^yEj&DxRG#4 z^{fA`mz%@vKk4nNy||(CBd>w&UIsA{wsZ9=%LCrb<XCAE5s<9V5VGNeMAIU7AvxYT zXWDE|q)u$)5$eA^;XFfR*cAu)g%ghkbh;jV=>G58i}l${r=9&jBjIwu&)0YK=H1_X z?lsrBn_6o(R`mU6+j8dcr6VUEtU3_?DCU1|(7kuJr++J&cl!S4=Bp0=8`3{pO>_0m zf3r(^#qN9c=Kq$@IXvm--Eea`tNo4%k8l0Y-X0U1tX=VH*G3=rlmCv#<$49`eYUt> z|8H03$`!>CKbckce|3FZtG?{L_0n~})pEj~UR(8G#+}9NRkb{)?C<ng-`W=XMPh%x z|M8^$O~z)=EX$v}iu{{$?#%rEfA_pSY`-)<dVj6f$BK<6OCG1!ulSTAcJtb+8P6Pa z*YBKNDRHi(_~Wu$Ie$&mPaQsNYU1^v;NjG0Id49x4;?>W>U=EYc$^g*_BGD_)62wS zR>!b;H;VfwrDw%?-BLfbXwS<#F>8;_6`hx}`H@-MuM?|x?k&7|^oq!(^s2`<j2HsE z8JTn$bYZ;@Pf$~W71Wkcn0$y&Paae*!fGc_%_IP+oS<apWPW~muo`fY%E-WQkdc7_ zRZZe#Uw(N|K|490Uz+jV<ZgZ?unur(%gVr@!^XfM2sIBv2~Ix4?;<#x55f!ZW@87j KE=^VxPy_&%ZF36% diff --git a/_scale-100.appx b/_scale-100.appx index 2ddc1d5eff625a5c24c37d149cad23525f3c2d45..622a8ba31e824c812aa8194d773b918e17dc8af6 100644 GIT binary patch delta 3539 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 LSgR&KE>r{n<DJge delta 3496 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 diff --git a/_scale-125.appx b/_scale-125.appx index 4696faa956d0bbfa2a9ea9acf8f212676ae44d47..45387f054f1bb902622e2d6903841b883c6ef60b 100644 GIT binary patch delta 3553 zcmcckhVkkfM&1B#W)@ur4h9B>;<EgWyossI#bx=E3sV*9Pv=X!O8h^z@$nw{vOCNC zX8Aea-V(yMO}hC(n$u;sA}#Z5Da9bm$zN|omdc-dr&jpD;XDWDKL)=)taEl0yliAv z&x=v!n<FSwwQ|Ct{Nt~;9##~pF!@z?|F_}h-P>YT-z)1?5?uU>Lr{HD_zg}gd-jF? zx)LT!@~-plerdD(;Qjj3^W+$%E?rK2{*(3Y<9YuMT;^Y*y5PC(-}49kTB^dUO1-yq zf8F=r{P2yyx`*6{r-$92TQMo){iI)UZ6?dE34|$4G5Db4qO&P|#R`en#kTQ_HgaEM z)e7SL^5*R8&8L6W%RJ?OcH)W@moj@`yT-aNj&r=9Pk9kqGJ8{Fth3o`_HFeWmauEf zm40~I-@aQya&K$yM0ZP}E5}}4VE@85jqTqI8KV=e>p1lsCTT4&lRcO>RqmqjuNm8> zE@74bB_DV2z1=t4H*vNH-}`TQ?e=-;Og+yFY^tB`p6pXPmQlx7oM_^i>h)=R=zZY> zhkq`y+G_bgv##BjIlW<dW#?I!PwhEolj^TWa9Qx$oRrq#o#S*>_)co^sbx&F^&{L) zZddp}E0%E|+ni_jH?2OkjBED$n>GbvThD$Be>3aUQpM}*bynAy{`hxJtRcPO_>cG; z!5>^7c>d3h4L8kLk!ZAS$p_;Htv7f@>)AiPKf2@7-NyI7H_m=BZ}Bq|eJ82q(g!s6 zHQr;6VXXh>a(51ERhN{{0*l!2op#4{9&h29^DsW~lcNl8;l9Nx1z}e|F!wSnU(q(x zf2D;?<z<_r%NoR9I!isZe<)*dcTwrAr~4ngD_+A@Q@^-y!S4f~|M1V@bicQJh2=@L zz}U7|CiCK*c(qT2F1Gluv29JBRdD)@z;gS(iq8i(bMKy4R?k|(`ej{}+OtBY$dc_H zFT7t=NPd4VmiJF~7w7!MFGfwjjs=`%d3Qp@H@Rr@3yt33lL@cFn=Pc<G;Ou2=Y6qV zWqTz{D%bnGWl?FzYxPT4WWDn$b+xwNUOo4(+SJm!mYe?G%8bs-x2xQ8Yg=@)QSRnd zE~4)B3tz6AQg$@c(yOHY#X4)=ohz-kC2ifNxu<!ZZ+Jn4N=&52Q|lWO!^3xNyA~P! z`gG0lse;?{xqi1galGZet1tgp^o3R@=i)ipH`dk{&g+_`{p53Erj=K40ek*(_SSCG zj>isbpL@MuYvv=I<18n=>&<81!(W!puHJs!YMttzeZNkapNUzy#NzUe>5uBaI0hPu zx|;;}g>G3Q^=_KM8#`rN{vB%8bt<cOozU@Ds*}F=y6>&!vNc@Mv%FSB9{T!w&GVUm zduG*lF~2|M|3OnQ?DmwM>&{t!EMd&^Ej(|)q3*Ti@?Or`dp~@3@Uk{Dy|n+^^!>-u z`<oeWzsoj|w3BwpaS)eZptEG-1iscS+v|lDdGBq0yinG=WN&ko-tW41yia&O-3fhB z_T$EzRmaoH9~7<B6zftgtvposUBuR&`=5Es{DY6|85kJ;|7T_h@Mh;oTyH+nf`tJL z0zgF!0|NuByx{<qH=bZAC?UYWz~ER=P~n@HmzkDYJo#THTm7W7d5;W4TE8owwT`%_ zt|obj@yQY|#}E%)C)PBx&4qKlH>hn?zP<3v+v>^F%w$WAPpR$N|Geh<pI<TIPgt&L zE)JGzRrYf54%UcWR<b{uP1tLW_%4B|7bgVez9{|nSNcB3&+sV@8)A1)k=vV})?-=O z618JP*d?iSKD#N)(iUabpIajqC3~Q<%s|)g^NxO7&Da_0;twy?om(S%w*Ks)Uk@jQ zuIv@<7hU7ZyG~FjK<e6sp7mT8;(n%IS{k%VRDE$yfxxLlb1%KM+!Om={(_0>>~9-` z-f@^M{ak9*`AEZmntikUXB)0#pKbV;ee{*-nSRn_r=;t}5}CkHubCC<7WdUZVz#&( z#kpE(>AICu_~(j<vINcvnD(*Pls97ifz&@izmA!F-o|?FU+<^ZLza(r8c$dlb?M~O z9EqpzZ?#`s7=CH?(>JG`>e9U?KRwr2Ucc?0f)r1S@AhT>iTA}fvF~R|Jil$Ozstw0 zDZHn|E;Jl-KC!P}J%_W7QC&4rGb=Q<?{mcOuZteWt$vr<)itSlsl9}9e2Ywf^||kl z{!F|1u)E=r?R=kg2KP08?3otZun#HXC+8-d*~kcq*FQ`U8YSW<-^y00&%b3La`e9X zTWgnl#)bbThqY*JIql{hwboNB?CVhp=RH>2I%9=?d@-Htu;$j|sqZYGm-oMWu_}MY zR@JqOn$!<$l}!-rJ38r8Dbp$==_3b%q&})9HBDwIU|e@Bet(ae&x7kSo~K{z^m16c zbx%vc!&xRCH+he)4M<v=t0UF1<junR=R5dfWKwu19r)E5GFLdZ`>E%h(z$nkZp*ly zcC&Dyu*2iLf_*EcDic|bE$%T&y1V$;ge6{rYvWG6ca7X*uu-h}n!{}_WjD5U;<qNP zZw$S#_vY6_7F`Xd(O2uAzj*Mq@DRH@Q{HxqtM+-)`M;%p-;Mk4xL@C_{PWq%Y4`1w zcYUs(vwVZ5n8}RCEC)Vv$T|EwnAs_mud8-yny&5BU2!#H(m&=Wr5;|!_fSda@WT*| zS#i%Ni&o$JcHQww%d7A^;h`~p`M*+U$=JrGOZ=BtG&OzRaalC*mf2ZZ(Zd=R?LTU& zZ*N}nB7OJHnh)Nu?tl62tfD>lynOd|&zUQ=s%PAF+$I+t!=m1Cp!~eVgOAe3-Xlfd zx#dgir%s-oD+y`~Y~G%$&B$3?mhWkP?+|~?<kxvq>z|*!eNw~BRjGK}(WkGb*q@&{ zecNYN8{hS}@2Eek+xa+DdV6xq=F_Rs-!}!{w%~E?%k<oR=gH3r)0cf$i%<Eve9Jp7 z$6mMf?GC1j*3+iFJA3hu`P~n5eNwo-KPhFJJ)3XV>YQB_bML=z{l8}=yR7n@n%nvI z8MjRn?~2`Tj-BshSMO(a?(biZU;kE?|G6}2`}3N|#kTtwbVeRo&>4APLFdQgOlfLv zYHEzi^S1@3+gon^QNJfz^3viLyq`Es7$kQ#9$b>YB74EvV=|`>>8wdg%t(2W@hWXy z_M0~uE3zNGd9flp^GV8*>_=%?nXgh3GBPrgG8EL$WoM*iB&1|zq$Q-)=SZ+Vnbo|* z%`HLiC7YR{mZCM6#I7^GJ%4x{3iXz$CH$%R@%ve{`1!9L_hZ>(a_@XQ{yF#k@1Hqq zLsv?D-okhB{nMbXZ{N1B{kPfuU+?T<i|YyRR=)pybIrM%^Y3lGW|pw~_4RjOYniNi zy-Q5WkFMj*v(5hb$-^rs<gw7U6<xaZbHli~SRc)kw6&R-y*csODgMo8*1MFr>x=hZ zjapuk{BotPKMSvw*{ra&_bc3AG+(*?$otPN|4sIL?@Tez{VHVbxqPPiKlL?{Wea^j z{8)Q6-&*bXsS3+?7U|~sPd`>9N+0Y{5C13kJIMO2+5c<r^+Z1}m%qB%I(cqY?6Zj< zd-LnH_1LFo$o{YL^m+LyHtgtR^WvUY@BZJPB)a4Iz4uEe9nHS+Yu?#8VtIA7@7MqP zmR)CWyl3VNNl6LuY>5?;9~hcL!)8qUXwN;FvAI#AV>#EOj}iqN3mGn(YHM2?8%MDi zH#co~)_?T)$#c_e&D<CHSgW<@%#MrwvFdZT-1}Mi*XzSmr=41Pk8|IoJq6nj9<A;^ z{3~VWwwRUX{pY!Lrwd%r=HI<xwQlV@rpENTPW{Pm?N@VM3zI3;-Z$^=(Ynve>z3Ee z{k#A7Th~lo_GP=yME?EncWq5&xT(dpl(U~!#LI2@Kj+HjV)kqAjkWG1ep_#5=5WSY z`&HUQ+vPj^y@WoSi_}}*dmg5@cjwo#sQYHylI7U9RQkt>aTSL}lpTyc>~zcR$cIG7 zKM${Oe!hi!`rBK_=g-|*{r`*R_vYMNed{Yv?nu7+D1H07pZQPc?^U^Y_w|MYg4>_J zPXAw7v+&i6KbnS`MTTblV$wdlmi+THuf6ul{hsS~eJL};eT>VNiyvq@bfA9b6Ok@k zVa2Iz+*v#<6S&0WoCCgi99rBs`Q`-nojOJn93K@*f09%<-6i!x*<_Q1)<zAVOXimH zPd_Yge(jO|_F=xJr0}eD_q#L8Lbq4k+rE2I-dEl&XAWOFazY^ObF4yY)ViieheYS! zQju2v^Tey~+nn3~-#<RaZSK$gYk$>;zx7?W)$)&O&wpYo$E6c?{+_|<m1U`o8-5!< zljY|-w=e8(_=fKv<`^4ge`Cpu46syIO)ZzNKXhta>if;5VV_q{%jv8NPhGa-b5`Mc z#{JV1Z(U!npZYEK?f0;|b+M5JzxQskoj&bVNp)=Xs+Id6=N)?;UcE`|o#8&E<e36H ze{5a9xPJHIvgPUj`_FcAUH-Fb&T7eCxubtP!wk(b%WcmT>W9DKo8EAHo^8E_y|2ab zLN~uvAFUQI__yAaO?-d&-Fl18TGu4+le3!7>TvVje*R3aC+2VZvyFe=neSh-ZR3pW zCE>qztlM>U`~F8Kmlhvl2=HcP(q({mkvut$ADmoS;>}xJmhTB`-~>#*SfVEn5`oox zpjr>qpn=qVAoBiXxl(zs8gTK-$iNW5#K3^6=Gx?#Qh89JJGrh@nsMXgS*1!~9pI9e lm4QKrje$WBWEdRZpM1O2MKG%bA`sxs#tveAo@`L22mm<czFz<U delta 3586 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 diff --git a/_scale-150.appx b/_scale-150.appx index 04caab6f5e3fc98964adb0732257c1a7bff852a7..0ffbcee757962fa987c0c1f112058b9fbd1c632b 100644 GIT binary patch delta 3524 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 OHyb;M)iU`&r6K^<FSqsp delta 3602 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 diff --git a/_scale-400.appx b/_scale-400.appx index 0b27623d07c12e86c82149aea9b233e170c6bc3f..37070f6429f51611a18c005fcf7414b22b2c2bc5 100644 GIT binary patch delta 3521 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 J^T`D(6am4b!BGGJ delta 3589 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 diff --git a/ring-client-uwp.vcxproj b/ring-client-uwp.vcxproj index 720d25d..8b532db 100644 --- a/ring-client-uwp.vcxproj +++ b/ring-client-uwp.vcxproj @@ -189,6 +189,9 @@ <ClInclude Include="MainPage.xaml.h"> <DependentUpon>MainPage.xaml</DependentUpon> </ClInclude> + <ClInclude Include="PreviewPage.xaml.h"> + <DependentUpon>PreviewPage.xaml</DependentUpon> + </ClInclude> <ClInclude Include="RingConsolePanel.xaml.h"> <DependentUpon>RingConsolePanel.xaml</DependentUpon> </ClInclude> @@ -227,6 +230,7 @@ <SubType>Designer</SubType> </Page> <Page Include="MessageTextPage.xaml" /> + <Page Include="PreviewPage.xaml" /> <Page Include="RingConsolePanel.xaml" /> <Page Include="SmartPanel.xaml" /> <Page Include="Styles.xaml" /> @@ -318,6 +322,9 @@ <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="PreviewPage.xaml.cpp"> + <DependentUpon>PreviewPage.xaml</DependentUpon> + </ClCompile> <ClCompile Include="RingConsolePanel.xaml.cpp"> <DependentUpon>RingConsolePanel.xaml</DependentUpon> </ClCompile> diff --git a/ring-client-uwp.vcxproj.filters b/ring-client-uwp.vcxproj.filters index 42897f4..562fb46 100644 --- a/ring-client-uwp.vcxproj.filters +++ b/ring-client-uwp.vcxproj.filters @@ -263,6 +263,9 @@ <Page Include="WelcomePage.xaml"> <Filter>Views</Filter> </Page> + <Page Include="PreviewPage.xaml"> + <Filter>Views</Filter> + </Page> </ItemGroup> <ItemGroup> <Filter Include="Assets"> -- GitLab