From dac8b81e45d3cb3a69e6883c9291a40cf7c8f7ce Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 22 Sep 2021 12:21:50 +0100 Subject: [PATCH 01/24] Bump to 302. --- Logic/ApplicationSettings.cs | 4 ++-- Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 5014354f..2874b725 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -51,9 +51,9 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R301.1"; + public string ClientVersion { get; } = "3 R302.0"; public string Version { get; } = "5"; - public int ClientCodeVersion { get; } = 301; + public int ClientCodeVersion { get; } = 302; private ApplicationSettings() { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 11197a9a..00f83d6e 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.301.1.0")] -[assembly: AssemblyFileVersion("3.301.1.0")] +[assembly: AssemblyVersion("3.302.0.0")] +[assembly: AssemblyFileVersion("3.302.0.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] From ab98490c2558b5b215be96bebd7e1b0196fc68d4 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 22 Sep 2021 12:22:37 +0100 Subject: [PATCH 02/24] Cef: Set RootCachePath and CachePath on request context. #226 --- MainWindow.xaml.cs | 1 + Rendering/WebCef.cs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index b0275eee..10877376 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -354,6 +354,7 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e) // Settings for Init CefSharp.Wpf.CefSettings settings = new CefSharp.Wpf.CefSettings { + RootCachePath = ApplicationSettings.Default.LibraryPath + @"\CEF", CachePath = ApplicationSettings.Default.LibraryPath + @"\CEF", LogFile = ApplicationSettings.Default.LibraryPath + @"\CEF\cef.log", LogSeverity = CefSharp.LogSeverity.Fatal diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index 96999c3e..a6e08a87 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -45,12 +45,16 @@ public override void RenderMedia(double position) { Debug.WriteLine("Created CEF Renderer for " + this.regionId, "WebCef"); + // Set a cache path + string cachePath = ApplicationSettings.Default.LibraryPath + @"\CEF"; + var requestContextSettings = new CefSharp.RequestContextSettings { CachePath = cachePath }; + // Create the web view we will use webView = new ChromiumWebBrowser() { Name = "region_" + this.regionId }; - webView.RequestContext = new CefSharp.RequestContext(); + webView.RequestContext = new CefSharp.RequestContext(requestContextSettings); // Configure run time CEF settings? if (!string.IsNullOrEmpty(ApplicationSettings.Default.AuthServerWhitelist) From cfb1316cd5d3383b541542316fe95a87a4dfe692 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 22 Sep 2021 12:49:39 +0100 Subject: [PATCH 03/24] Always provide our own RequestHandler to catch terminated sub processes #227 --- ...equestHandler.cs => XiboRequestHandler.cs} | 20 +++++++-- Rendering/WebCef.cs | 41 ++++++++----------- XiboClient.csproj | 2 +- 3 files changed, 35 insertions(+), 28 deletions(-) rename Helpers/{ProxyRequestHandler.cs => XiboRequestHandler.cs} (63%) diff --git a/Helpers/ProxyRequestHandler.cs b/Helpers/XiboRequestHandler.cs similarity index 63% rename from Helpers/ProxyRequestHandler.cs rename to Helpers/XiboRequestHandler.cs index 079da60d..cd92dac2 100644 --- a/Helpers/ProxyRequestHandler.cs +++ b/Helpers/XiboRequestHandler.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -20,14 +20,28 @@ */ using CefSharp; using CefSharp.Handler; +using System.Diagnostics; namespace XiboClient.Helpers { - class ProxyRequestHandler : RequestHandler + class XiboRequestHandler : RequestHandler { + private bool _isConfigureProxy; + + public XiboRequestHandler(bool isConfigureProxy) + { + _isConfigureProxy = isConfigureProxy; + } + + protected override void OnRenderProcessTerminated(IWebBrowser chromiumWebBrowser, IBrowser browser, CefTerminationStatus status) + { + // If the render process crashed, we should just log. + Trace.WriteLine(new LogMessage("XiboRequestHandler", "OnRenderProcessTerminate: a cef sub process has terminated. " + status.ToString()), LogType.Error.ToString()); + } + protected override bool GetAuthCredentials(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) { - if (isProxy) + if (_isConfigureProxy && isProxy) { callback.Continue(ApplicationSettings.Default.ProxyUser, ApplicationSettings.Default.ProxyPassword); diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index a6e08a87..3d8c065b 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -57,39 +57,32 @@ public override void RenderMedia(double position) webView.RequestContext = new CefSharp.RequestContext(requestContextSettings); // Configure run time CEF settings? - if (!string.IsNullOrEmpty(ApplicationSettings.Default.AuthServerWhitelist) - || !string.IsNullOrEmpty(ApplicationSettings.Default.ProxyUser)) + CefSharp.Cef.UIThreadTaskFactory.StartNew(() => { - CefSharp.Cef.UIThreadTaskFactory.StartNew(() => + try { - try + // Provide our own request handler. + webView.RequestHandler = new XiboRequestHandler(!string.IsNullOrEmpty(ApplicationSettings.Default.ProxyUser)); + + // NTLM/Auth Server White Lists. + if (!string.IsNullOrEmpty(ApplicationSettings.Default.AuthServerWhitelist)) { - // NTLM/Auth Server White Lists. - if (!string.IsNullOrEmpty(ApplicationSettings.Default.AuthServerWhitelist)) + if (!webView.RequestContext.SetPreference("auth.server_whitelist", ApplicationSettings.Default.AuthServerWhitelist, out string error)) { - if (!webView.RequestContext.SetPreference("auth.server_whitelist", ApplicationSettings.Default.AuthServerWhitelist, out string error)) - { - Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: auth.server_whitelist. e = " + error), LogType.Info.ToString()); - } - - if (!webView.RequestContext.SetPreference("auth.negotiate_delegate_whitelist", ApplicationSettings.Default.AuthServerWhitelist, out string error2)) - { - Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: auth.negotiate_delegate_whitelist. e = " + error2), LogType.Info.ToString()); - } + Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: auth.server_whitelist. e = " + error), LogType.Info.ToString()); } - // Proxy - if (!string.IsNullOrEmpty(ApplicationSettings.Default.ProxyUser)) + if (!webView.RequestContext.SetPreference("auth.negotiate_delegate_whitelist", ApplicationSettings.Default.AuthServerWhitelist, out string error2)) { - webView.RequestHandler = new ProxyRequestHandler(); + Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: auth.negotiate_delegate_whitelist. e = " + error2), LogType.Info.ToString()); } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: Exception setting auto policies on cef. e = " + e.Message), LogType.Info.ToString()); } - }); - } + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("WebCef", "RenderMedia: Exception setting auto policies on cef. e = " + e.Message), LogType.Info.ToString()); + } + }); webView.Visibility = System.Windows.Visibility.Hidden; webView.Loaded += WebView_Loaded; diff --git a/XiboClient.csproj b/XiboClient.csproj index d9f8cfc2..fc44ec2c 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -126,7 +126,7 @@ - + InfoScreen.xaml From 9de0094918202d595d66b38c7c70c6a639a4fc02 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 22 Sep 2021 13:15:12 +0100 Subject: [PATCH 04/24] Bump cefshapr and webview2 packages. Disable pinch. #228 --- Logic/MediaOptions.cs | 2 ++ MainWindow.xaml.cs | 1 + Rendering/Media.xaml.cs | 6 ++++++ Rendering/WebEdge.cs | 1 + XiboClient.csproj | 4 ++-- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Logic/MediaOptions.cs b/Logic/MediaOptions.cs index 1f137bfd..f1a6726e 100644 --- a/Logic/MediaOptions.cs +++ b/Logic/MediaOptions.cs @@ -99,6 +99,8 @@ public List Audio /// public bool isStatEnabled; + public bool IsPinchToZoomEnabled { get; set; } + /// /// Decorate this Media Options with Region Options. /// diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 10877376..b0023936 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -360,6 +360,7 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e) LogSeverity = CefSharp.LogSeverity.Fatal }; settings.CefCommandLineArgs["autoplay-policy"] = "no-user-gesture-required"; + settings.CefCommandLineArgs["disable-pinch"] = "1"; CefSharp.Cef.Initialize(settings); } diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index af608adb..331c7e99 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -619,6 +619,7 @@ public static Media Create(MediaOptions options) break; case "embedded": + options.IsPinchToZoomEnabled = true; media = WebMedia.GetConfiguredWebMedia(options, WebMedia.ReadBrowserType(options.text)); break; @@ -629,6 +630,7 @@ public static Media Create(MediaOptions options) break; case "webpage": + options.IsPinchToZoomEnabled = true; media = WebMedia.GetConfiguredWebMedia(options); break; @@ -642,6 +644,7 @@ public static Media Create(MediaOptions options) break; case "htmlpackage": + options.IsPinchToZoomEnabled = true; media = WebMedia.GetConfiguredWebMedia(options); ((WebMedia)media).ConfigureForHtmlPackage(); break; @@ -704,6 +707,9 @@ public static MediaOptions ParseOptions(XmlNode node) // Stats enabled? options.isStatEnabled = (nodeAttributes["enableStat"] == null) ? true : (int.Parse(nodeAttributes["enableStat"].Value) == 1); + // Pinch to Zoom enabled? + options.IsPinchToZoomEnabled = false; + // Parse the options for this media node // Type and Duration will always be on the media node options.type = nodeAttributes["type"].Value; diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs index ada2587c..4fbcd86a 100644 --- a/Rendering/WebEdge.cs +++ b/Rendering/WebEdge.cs @@ -57,6 +57,7 @@ public WebEdge(MediaOptions options) : base(options) DefaultBackgroundColor = System.Drawing.Color.Transparent, Focusable = false, }; + this.webView.CoreWebView2.Settings.IsPinchZoomEnabled = options.IsPinchToZoomEnabled; this.webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted; this.webView.NavigationCompleted += WebView_NavigationCompleted; diff --git a/XiboClient.csproj b/XiboClient.csproj index fc44ec2c..93222ddd 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -292,7 +292,7 @@ 1.8.9 - 91.1.211 + 93.1.140 1.2.0 @@ -322,7 +322,7 @@ 14.0.1016.290 - 1.0.864.35 + 1.0.961.33 4.0.1.6 From f845d9ba69d05f85db000360dd6fcf86c6ea5ad8 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Oct 2021 14:33:49 +0100 Subject: [PATCH 05/24] Interactive: action not dismissing correctly #232 --- MainWindow.xaml.cs | 8 +------- Rendering/Layout.xaml.cs | 18 ------------------ Rendering/Region.xaml.cs | 18 ++++++++---------- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index b0023936..27f2ea88 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1014,17 +1014,11 @@ public void HandleActionTrigger(string triggerType, string triggerCode, int sour continue; } // If the source of the action is a widget, it must currently be active. - else if (triggerType == "touch" && !action.IsDrawer && action.Source == "widget" && currentLayout.GetCurrentWidgetIdForRegion(point) != "" + action.SourceId) + else if (triggerType == "touch" && action.Source == "widget" && currentLayout.GetCurrentWidgetIdForRegion(point) != "" + action.SourceId) { Debug.WriteLine(point.ToString() + " not active widget: " + action.SourceId, "HandleActionTrigger"); continue; } - // If for a drawer widget, it has to be active - else if (triggerType == "touch" && action.IsDrawer && action.Source == "widget" && currentLayout.GetCurrentInteractiveWidgetIdForRegion(point) != ""+action.SourceId) - { - Debug.WriteLine(point.ToString() + " not active drawer widget: " + action.SourceId, "HandleActionTrigger"); - continue; - } // Action found, so execute it try diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 83c40920..73ca4d1d 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -577,24 +577,6 @@ public string GetCurrentWidgetIdForRegion(Point point) return null; } - /// - /// Get Current Widget Id for the provided Region - /// - /// - /// - public string GetCurrentInteractiveWidgetIdForRegion(Point point) - { - foreach (Region region in _regions) - { - if (region.Dimensions.Contains(point)) - { - return region.GetCurrentInteractiveWidgetId(); - } - } - - return null; - } - /// /// Execute a Widget /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index a0c4bf35..3abb5417 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -141,16 +141,14 @@ public void LoadFromOptions(string id, RegionOptions options, XmlNodeList media) /// public string GetCurrentWidgetId() { - return this.currentMedia?.Id; - } - - /// - /// Get Current WidgetId - /// - /// - public string GetCurrentInteractiveWidgetId() - { - return this.navigatedMedia?.Id; + if (this.navigatedMedia != null) + { + return this.navigatedMedia.Id; + } + else + { + return this.currentMedia?.Id; + } } /// From 881306a398fc0c0a9bef44a1a9bb5bd85ea2d4e3 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Oct 2021 16:40:09 +0100 Subject: [PATCH 06/24] Additional logging for cef load error --- Rendering/WebCef.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index 3d8c065b..4c9ff996 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -162,9 +162,9 @@ private void WebView_Loaded(object sender, System.Windows.RoutedEventArgs e) /// private void WebView_LoadError(object sender, CefSharp.LoadErrorEventArgs e) { - Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ToString()), LogType.Error.ToString()); + Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ErrorText + ", code = " + e.ErrorCode), LogType.Error.ToString()); - // This should exipre the media + // This should expire the media Duration = 5; base.RestartTimer(); } From 2f33defac2765e2b27472c3caea81c17e5309b76 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 22 Oct 2021 10:36:05 +0100 Subject: [PATCH 07/24] Ignore cef aborted error statuses. #235 --- Rendering/WebCef.cs | 33 ++++++++++++++++++++++++++------- Rendering/WebEdge.cs | 10 ++++++++++ XiboClient.csproj | 4 ++-- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index 4c9ff996..25e922af 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -28,8 +28,13 @@ namespace XiboClient.Rendering class WebCef : WebMedia { private ChromiumWebBrowser webView; - private string regionId; - private bool hasBackgroundColor = false; + private readonly string regionId; + private readonly bool hasBackgroundColor = false; + + /// + /// A flag to indicate whether we have loaded web content or not. + /// + private bool hasLoaded = false; public WebCef(MediaOptions options) : base(options) @@ -125,8 +130,11 @@ private void WebView_FrameLoadEnd(object sender, CefSharp.FrameLoadEndEventArgs { Debug.WriteLine(DateTime.Now.ToLongTimeString() + " Frame Loaded", "CefWebView"); + // Flag that we've opened. + hasLoaded = true; + // If we aren't expired yet, we should show it - if (e.Frame.IsMain && !Expired) + if (e.Frame.IsMain && !Expired && !IsNativeOpen()) { // Initialise Interactive Control webView.GetBrowser().MainFrame.ExecuteJavaScriptAsync("xiboIC.config({hostname:\"localhost\", port: " @@ -143,6 +151,9 @@ private void WebView_Loaded(object sender, System.Windows.RoutedEventArgs e) { Debug.WriteLine(DateTime.Now.ToLongTimeString() + " Navigate Completed", "CefWebView"); + // Flag that we've opened. + hasLoaded = true; + // Show the browser after some time if (!Expired) { @@ -162,11 +173,19 @@ private void WebView_Loaded(object sender, System.Windows.RoutedEventArgs e) /// private void WebView_LoadError(object sender, CefSharp.LoadErrorEventArgs e) { - Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ErrorText + ", code = " + e.ErrorCode), LogType.Error.ToString()); + // We are not interested in aborted errors. + if (e.ErrorCode == CefSharp.CefErrorCode.Aborted && hasLoaded) + { + Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Abort received, ignoring."), LogType.Audit.ToString()); + } + else + { + Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ErrorText + ", code = " + e.ErrorCode), LogType.Error.ToString()); - // This should expire the media - Duration = 5; - base.RestartTimer(); + // This should expire the media + Duration = 5; + base.RestartTimer(); + } } /// diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs index 4fbcd86a..4c88318f 100644 --- a/Rendering/WebEdge.cs +++ b/Rendering/WebEdge.cs @@ -36,6 +36,11 @@ class WebEdge : WebMedia private readonly WebView2 webView; private bool _webViewInitialised = false; private bool _webViewError = false; + + /// + /// A flag to indicate whether we have loaded web content or not. + /// + private bool hasLoaded = false; private readonly bool hasBackgroundColor = false; private bool _renderCalled = false; @@ -194,6 +199,7 @@ private void WebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.C if (e.IsSuccess) { Debug.WriteLine("WebView_NavigationCompleted: Navigate Completed", "WebView"); + hasLoaded = true; DocumentCompleted(); @@ -201,6 +207,10 @@ private void WebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.C webView.ExecuteScriptAsync("xiboIC.config({hostname:\"localhost\", port: " + ApplicationSettings.Default.EmbeddedServerPort + "})"); } + else if (hasLoaded && e.WebErrorStatus == CoreWebView2WebErrorStatus.ConnectionAborted) + { + Trace.WriteLine(new LogMessage("WebView", "WebView_LoadError: Abort received, ignoring."), LogType.Audit.ToString()); + } else { Trace.WriteLine(new LogMessage("WebView", "WebView_NavigationCompleted: e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); diff --git a/XiboClient.csproj b/XiboClient.csproj index 93222ddd..9bab6247 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -292,7 +292,7 @@ 1.8.9 - 93.1.140 + 94.4.50 1.2.0 @@ -322,7 +322,7 @@ 14.0.1016.290 - 1.0.961.33 + 1.0.992.28 4.0.1.6 From 1cddd4c1c631820795871a41e947846793240ea7 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 22 Oct 2021 12:42:34 +0100 Subject: [PATCH 08/24] Fix pinch to zoom for webview2 when init is slow #228 --- Rendering/WebEdge.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs index 4c88318f..ec6badae 100644 --- a/Rendering/WebEdge.cs +++ b/Rendering/WebEdge.cs @@ -43,6 +43,7 @@ class WebEdge : WebMedia private bool hasLoaded = false; private readonly bool hasBackgroundColor = false; + private readonly bool isPinchToZoomEnabled = false; private bool _renderCalled = false; private double _position; @@ -62,9 +63,9 @@ public WebEdge(MediaOptions options) : base(options) DefaultBackgroundColor = System.Drawing.Color.Transparent, Focusable = false, }; - this.webView.CoreWebView2.Settings.IsPinchZoomEnabled = options.IsPinchToZoomEnabled; this.webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted; this.webView.NavigationCompleted += WebView_NavigationCompleted; + this.isPinchToZoomEnabled = options.IsPinchToZoomEnabled; // Initialise the web view InitialiseWebView(); @@ -127,6 +128,7 @@ private void WebView_CoreWebView2InitializationCompleted(object sender, Microsof { if (e.IsSuccess) { + webView.CoreWebView2.Settings.IsPinchZoomEnabled = isPinchToZoomEnabled; _webViewInitialised = true; } else From 72ed3c074348db454d291856340d63e6597cedeb Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 25 Oct 2021 17:09:16 +0100 Subject: [PATCH 09/24] Video: restart timer on open media when not end detect (this shouldn't matter but incase there is a delay opening the media we do it) --- Rendering/Video.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Rendering/Video.cs b/Rendering/Video.cs index ec051482..f737f71f 100644 --- a/Rendering/Video.cs +++ b/Rendering/Video.cs @@ -280,6 +280,13 @@ private void MediaElement_MediaOpened(object sender, RoutedEventArgs e) // Open has been called. this._openCalled = true; + // If we have been given a duration, restart the timer + // we are trying to cater for any time lost opening the media + if (!_detectEnd) + { + RestartTimer(); + } + // Try to seek if (this._position > 0) { From b45eeaac8a1e34a4a15f108c84eb2215e283679c Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 25 Oct 2021 17:11:22 +0100 Subject: [PATCH 10/24] Update to XMDS v6 #230. First pass at faults #229. --- Logic/ApplicationSettings.cs | 4 +- Logic/CacheManager.cs | 96 ++++++++++++++- Logic/Schedule.cs | 22 ++++ Properties/AssemblyInfo.cs | 4 +- Properties/Settings.Designer.cs | 4 +- Properties/Settings.settings | 2 +- Rendering/Layout.xaml.cs | 2 +- Rendering/PowerPoint.cs | 2 +- Rendering/Region.xaml.cs | 2 +- Rendering/Video.cs | 4 +- Web References/xmds/Reference.cs | 88 +++++++------- Web References/xmds/Reference.map | 2 +- .../xmds/{service_v5.wsdl => service_v6.wsdl} | 20 ++-- XiboClient.csproj | 7 +- XmdsAgents/FaultsAgent.cs | 111 ++++++++++++++++++ app.config | 2 +- 16 files changed, 294 insertions(+), 78 deletions(-) rename Web References/xmds/{service_v5.wsdl => service_v6.wsdl} (95%) create mode 100644 XmdsAgents/FaultsAgent.cs diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 2874b725..73510274 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -51,8 +51,8 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R302.0"; - public string Version { get; } = "5"; + public string ClientVersion { get; } = "3 R302.2"; + public string Version { get; } = "6"; public int ClientCodeVersion { get; } = 302; private ApplicationSettings() diff --git a/Logic/CacheManager.cs b/Logic/CacheManager.cs index 22fcbddb..d788079f 100644 --- a/Logic/CacheManager.cs +++ b/Logic/CacheManager.cs @@ -18,12 +18,15 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Xml; using System.Xml.Serialization; using XiboClient.Log; @@ -342,9 +345,9 @@ public void Regenerate() /// /// /// - public void AddUnsafeItem(UnsafeItemType type, int layoutId, string id, string reason) + public void AddUnsafeItem(UnsafeItemType type, UnsafeFaultCodes code, int layoutId, string id, string reason) { - AddUnsafeItem(type, layoutId, id, reason, 86400); + AddUnsafeItem(type, code, layoutId, id, reason, 86400); } /// @@ -355,7 +358,7 @@ public void AddUnsafeItem(UnsafeItemType type, int layoutId, string id, string r /// /// /// - public void AddUnsafeItem(UnsafeItemType type, int layoutId, string id, string reason, int ttl) + public void AddUnsafeItem(UnsafeItemType type, UnsafeFaultCodes code, int layoutId, string id, string reason, int ttl) { if (ttl == 0) { @@ -369,6 +372,7 @@ public void AddUnsafeItem(UnsafeItemType type, int layoutId, string id, string r .First(); item.DateTime = DateTime.Now; + item.Code = code; item.Reason = reason; } catch @@ -377,6 +381,7 @@ public void AddUnsafeItem(UnsafeItemType type, int layoutId, string id, string r { DateTime = DateTime.Now, Type = type, + Code = code, LayoutId = layoutId, Id = id, Reason = reason, @@ -487,11 +492,75 @@ private string UnsafeListAsString() string list = ""; foreach (UnsafeItem item in _unsafeItems) { - list += item.Type.ToString() + ": " + item.LayoutId + ", " + item.Reason + ", ttl: " + item.Ttl + Environment.NewLine; + list += item.Type.ToString() + ": " + item.LayoutId + ", [" + (int)item.Code + "] " + item.Reason + ", ttl: " + item.Ttl + Environment.NewLine; } return list; } + /// + /// Get the unsafe list as a JSON string + /// + /// + public string UnsafeListAsJsonString() + { + if (_unsafeItems.Count <= 0) + { + return "[]"; + } + + // Go through each and add a JSON string. + StringBuilder sb = new StringBuilder(); + using (StringWriter sw = new StringWriter(sb)) + { + using (JsonWriter writer = new JsonTextWriter(sw)) + { + writer.Formatting = Newtonsoft.Json.Formatting.None; + writer.WriteStartArray(); + + foreach (UnsafeItem item in _unsafeItems) + { + writer.WriteStartObject(); + writer.WritePropertyName("date"); + writer.WriteValue(item.DateTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); + + writer.WritePropertyName("expires"); + writer.WriteValue(item.DateTime.AddSeconds(item.Ttl).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); + + writer.WritePropertyName("code"); + writer.WriteValue((int)item.Code); + + writer.WritePropertyName("reason"); + writer.WriteValue(item.Reason); + + // IDs + if (item.Type == UnsafeItemType.Media) + { + writer.WritePropertyName("mediaId"); + writer.WriteValue(item.Id); + } + else if (item.Type == UnsafeItemType.Widget) + { + writer.WritePropertyName("widgetId"); + writer.WriteValue(item.Id); + } + else if (item.Type == UnsafeItemType.Region) + { + writer.WritePropertyName("regionId"); + writer.WriteValue(item.Id); + } + + writer.WritePropertyName("layoutId"); + writer.WriteValue(item.LayoutId); + + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + } + + return sb.ToString(); + } + #endregion #region Layout Codes @@ -598,6 +667,7 @@ public struct UnsafeItem { public DateTime DateTime { get; set; } public UnsafeItemType Type { get; set; } + public UnsafeFaultCodes Code { get; set; } public int LayoutId { get; set; } public string Id { get; set; } public string Reason { get; set; } @@ -615,6 +685,24 @@ public enum UnsafeItemType Media } + /// + /// Unsafe fault codes + /// + public enum UnsafeFaultCodes + { + NotLicensed=1000, + MemoryRunningLow=1001, + MemoryCritical=1002, + PowerPointNotAvailable=1003, + VideoSource=2001, + VideoUnexpected=2099, + ImageUnknown=3000, + ImageDecode=3001, + ImageOutOfMemory=3002, + RemteResourceFailed=4404, + XlfNoContent=5000 + } + /// /// Layout Codes /// diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index f7c8ab7f..e28f242f 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -85,6 +85,10 @@ public class Schedule private LogAgent _logAgent; Thread _logAgentThread; + // Faults Agent + private FaultsAgent _faultsAgent; + Thread _faultsAgentThread; + // XMR Subscriber private XmrSubscriber _xmrSubscriber; Thread _xmrSubscriberThread; @@ -149,6 +153,16 @@ public Schedule(string scheduleLocation) _logAgentThread = new Thread(new ThreadStart(_logAgent.Run)); _logAgentThread.Name = "LogAgent"; + // Faults Agent + _faultsAgent = new FaultsAgent + { + HardwareKey = _hardwareKey.Key + }; + _faultsAgentThread = new Thread(new ThreadStart(_faultsAgent.Run)) + { + Name = "FaultsAgent" + }; + // XMR Subscriber _xmrSubscriber = new XmrSubscriber(); _xmrSubscriber.HardwareKey = _hardwareKey; @@ -189,6 +203,9 @@ public void InitializeComponents() // Start the LogAgent thread _logAgentThread.Start(); + // Start the Faults thread + _faultsAgentThread.Start(); + // Start the Proof of Play thread StatManager.Instance.Start(); @@ -274,6 +291,7 @@ private bool agentThreadsAlive() return _registerAgentThread.IsAlive && _scheduleAndRfAgentThread.IsAlive && _logAgentThread.IsAlive && + _faultsAgentThread.IsAlive && _libraryAgentThread.IsAlive && _xmrSubscriberThread.IsAlive; } @@ -373,6 +391,7 @@ public void wakeUpXmds() _registerAgent.WakeUp(); _scheduleAndRfAgent.WakeUp(); _logAgent.WakeUp(); + _faultsAgent.WakeUp(); } /// @@ -571,6 +590,9 @@ public void Stop() // Stop the LogAgent Thread _logAgent.Stop(); + // Stop the Faults Agent Thread + _faultsAgent.Stop(); + // Stop the Proof of Play Thread StatManager.Instance.Stop(); diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 00f83d6e..eb617008 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.302.0.0")] -[assembly: AssemblyFileVersion("3.302.0.0")] +[assembly: AssemblyVersion("3.302.2.0")] +[assembly: AssemblyFileVersion("3.302.2.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index c47d68c2..f5781fe0 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace XiboClient.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -26,7 +26,7 @@ public static Settings Default { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("http://localhost/xmds.php?v=5")] + [global::System.Configuration.DefaultSettingValueAttribute("http://localhost/xmds.php?v=6")] public string XiboClient_xmds_xmds { get { return ((string)(this["XiboClient_xmds_xmds"])); diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 13516905..88f22b3f 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -3,7 +3,7 @@ - http://localhost/xmds.php?v=5 + http://localhost/xmds.php?v=6 \ No newline at end of file diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 73ca4d1d..c4fab558 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -285,7 +285,7 @@ public void loadFromFile(ScheduleItem scheduleItem) LogType.Info.ToString()); // Add this to our unsafe list. - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Layout, _layoutId, ""+_layoutId, "No Regions or Widgets"); + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Layout, UnsafeFaultCodes.XlfNoContent, _layoutId, ""+_layoutId, "No Regions or Widgets"); throw new LayoutInvalidException("Layout without any Regions or Widgets"); } diff --git a/Rendering/PowerPoint.cs b/Rendering/PowerPoint.cs index 0a498be5..869eeea7 100644 --- a/Rendering/PowerPoint.cs +++ b/Rendering/PowerPoint.cs @@ -10,7 +10,7 @@ public PowerPoint(MediaOptions options) : base(options) // Check if PowerPoint is enabled if (!ApplicationSettings.Default.PowerpointEnabled) { - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, options.layoutId, options.mediaid, "PowerPoint not enabled on this Display", 300); + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, UnsafeFaultCodes.PowerPointNotAvailable, options.layoutId, options.mediaid, "PowerPoint not enabled on this Display", 300); throw new Exception("PowerPoint not enabled on this Display"); } diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 3abb5417..ca9bc049 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -315,7 +315,7 @@ private void StartNext(double position) if (!SetNextMediaNodeInOptions()) { // For some reason we cannot set a media node... so we need this region to become invalid - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Region, options.layoutId, options.regionId, "Unable to set any region media nodes.", _widgetAvailableTtl); + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Region, UnsafeFaultCodes.XlfNoContent, options.layoutId, options.regionId, "Unable to set any region media nodes.", _widgetAvailableTtl); // Throw this out so we remove the Layout throw new InvalidOperationException("Unable to set any region media nodes."); diff --git a/Rendering/Video.cs b/Rendering/Video.cs index f737f71f..4cc0f6ca 100644 --- a/Rendering/Video.cs +++ b/Rendering/Video.cs @@ -108,7 +108,7 @@ private void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e) this._openCalled = true; // Add this to a temporary blacklist so that we don't repeat it too quickly - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, LayoutId, Id, "Video Failed: " + e.ErrorException.Message, 120); + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, UnsafeFaultCodes.VideoUnexpected, LayoutId, Id, "Video Failed: " + e.ErrorException.Message, 120); // Expire SignalElapsedEvent(); @@ -167,7 +167,7 @@ private void MediaElement_Loaded(object sender, RoutedEventArgs e) Trace.WriteLine(new LogMessage("Video", "MediaElement_Loaded: " + this.Id + " Open not called after 4 seconds, marking unsafe and Expiring."), LogType.Error.ToString()); // Add this to a temporary blacklist so that we don't repeat it too quickly - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, LayoutId, Id, "Video Failed: Open not called after 4 seconds", 120); + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, UnsafeFaultCodes.VideoUnexpected, LayoutId, Id, "Video Failed: Open not called after 4 seconds", 120); // Expire SignalElapsedEvent(); diff --git a/Web References/xmds/Reference.cs b/Web References/xmds/Reference.cs index 67c0cebc..db726fda 100644 --- a/Web References/xmds/Reference.cs +++ b/Web References/xmds/Reference.cs @@ -23,7 +23,7 @@ namespace XiboClient.xmds { /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="xmdsBinding", Namespace="urn:xmds")] @@ -37,7 +37,7 @@ public partial class xmds : System.Web.Services.Protocols.SoapHttpClientProtocol private System.Threading.SendOrPostCallback ScheduleOperationCompleted; - private System.Threading.SendOrPostCallback BlackListOperationCompleted; + private System.Threading.SendOrPostCallback ReportFaultsOperationCompleted; private System.Threading.SendOrPostCallback SubmitLogOperationCompleted; @@ -102,7 +102,7 @@ public xmds() { public event ScheduleCompletedEventHandler ScheduleCompleted; /// - public event BlackListCompletedEventHandler BlackListCompleted; + public event ReportFaultsCompletedEventHandler ReportFaultsCompleted; /// public event SubmitLogCompletedEventHandler SubmitLogCompleted; @@ -275,40 +275,36 @@ private void OnScheduleOperationCompleted(object arg) { } /// - [System.Web.Services.Protocols.SoapRpcMethodAttribute("urn:xmds#BlackList", RequestNamespace="urn:xmds", ResponseNamespace="urn:xmds")] + [System.Web.Services.Protocols.SoapRpcMethodAttribute("urn:xmds#ReportFaults", RequestNamespace="urn:xmds", ResponseNamespace="urn:xmds")] [return: System.Xml.Serialization.SoapElementAttribute("success")] - public bool BlackList(string serverKey, string hardwareKey, int mediaId, string type, string reason) { - object[] results = this.Invoke("BlackList", new object[] { + public bool ReportFaults(string serverKey, string hardwareKey, string fault) { + object[] results = this.Invoke("ReportFaults", new object[] { serverKey, hardwareKey, - mediaId, - type, - reason}); + fault}); return ((bool)(results[0])); } /// - public void BlackListAsync(string serverKey, string hardwareKey, int mediaId, string type, string reason) { - this.BlackListAsync(serverKey, hardwareKey, mediaId, type, reason, null); + public void ReportFaultsAsync(string serverKey, string hardwareKey, string fault) { + this.ReportFaultsAsync(serverKey, hardwareKey, fault, null); } /// - public void BlackListAsync(string serverKey, string hardwareKey, int mediaId, string type, string reason, object userState) { - if ((this.BlackListOperationCompleted == null)) { - this.BlackListOperationCompleted = new System.Threading.SendOrPostCallback(this.OnBlackListOperationCompleted); + public void ReportFaultsAsync(string serverKey, string hardwareKey, string fault, object userState) { + if ((this.ReportFaultsOperationCompleted == null)) { + this.ReportFaultsOperationCompleted = new System.Threading.SendOrPostCallback(this.OnReportFaultsOperationCompleted); } - this.InvokeAsync("BlackList", new object[] { + this.InvokeAsync("ReportFaults", new object[] { serverKey, hardwareKey, - mediaId, - type, - reason}, this.BlackListOperationCompleted, userState); + fault}, this.ReportFaultsOperationCompleted, userState); } - private void OnBlackListOperationCompleted(object arg) { - if ((this.BlackListCompleted != null)) { + private void OnReportFaultsOperationCompleted(object arg) { + if ((this.ReportFaultsCompleted != null)) { System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.BlackListCompleted(this, new BlackListCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); + this.ReportFaultsCompleted(this, new ReportFaultsCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); } } @@ -540,11 +536,11 @@ private bool IsLocalFileSystemWebService(string url) { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void RegisterDisplayCompletedEventHandler(object sender, RegisterDisplayCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class RegisterDisplayCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -566,11 +562,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void RequiredFilesCompletedEventHandler(object sender, RequiredFilesCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class RequiredFilesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -592,11 +588,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void GetFileCompletedEventHandler(object sender, GetFileCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class GetFileCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -618,11 +614,11 @@ public byte[] Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void ScheduleCompletedEventHandler(object sender, ScheduleCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class ScheduleCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -644,18 +640,18 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] - public delegate void BlackListCompletedEventHandler(object sender, BlackListCompletedEventArgs e); + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] + public delegate void ReportFaultsCompletedEventHandler(object sender, ReportFaultsCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class BlackListCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { + public partial class ReportFaultsCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { private object[] results; - internal BlackListCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : + internal ReportFaultsCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : base(exception, cancelled, userState) { this.results = results; } @@ -670,11 +666,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void SubmitLogCompletedEventHandler(object sender, SubmitLogCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitLogCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -696,11 +692,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void SubmitStatsCompletedEventHandler(object sender, SubmitStatsCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitStatsCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -722,11 +718,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void MediaInventoryCompletedEventHandler(object sender, MediaInventoryCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class MediaInventoryCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -748,11 +744,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void GetResourceCompletedEventHandler(object sender, GetResourceCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class GetResourceCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -774,11 +770,11 @@ public string Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void NotifyStatusCompletedEventHandler(object sender, NotifyStatusCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class NotifyStatusCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -800,11 +796,11 @@ public bool Result { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] public delegate void SubmitScreenShotCompletedEventHandler(object sender, SubmitScreenShotCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.3752.0")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.8.4084.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitScreenShotCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { diff --git a/Web References/xmds/Reference.map b/Web References/xmds/Reference.map index b4eab380..75283d4e 100644 --- a/Web References/xmds/Reference.map +++ b/Web References/xmds/Reference.map @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Web References/xmds/service_v5.wsdl b/Web References/xmds/service_v6.wsdl similarity index 95% rename from Web References/xmds/service_v5.wsdl rename to Web References/xmds/service_v6.wsdl index 1adda03d..409074a2 100644 --- a/Web References/xmds/service_v5.wsdl +++ b/Web References/xmds/service_v6.wsdl @@ -46,14 +46,12 @@ - + - - - + - + @@ -127,10 +125,10 @@ - - Set media to be blacklisted - - + + Report Player faults + + Submit Logging from the Client @@ -201,8 +199,8 @@ - - + + diff --git a/XiboClient.csproj b/XiboClient.csproj index 9bab6247..fa8ab136 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -184,6 +184,7 @@ OptionsForm.xaml + @@ -254,7 +255,7 @@ MSDiscoCodeGenerator Reference.cs - + @@ -275,10 +276,10 @@ - + Dynamic Web References\xmds\ - https://raw.githubusercontent.com/xibosignage/xibo-cms/develop/lib/Xmds/service_v5.wsdl + https://raw.githubusercontent.com/xibosignage/xibo-cms/develop/lib/Xmds/service_v6.wsdl diff --git a/XmdsAgents/FaultsAgent.cs b/XmdsAgents/FaultsAgent.cs new file mode 100644 index 00000000..219cab14 --- /dev/null +++ b/XmdsAgents/FaultsAgent.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace XiboClient.XmdsAgents +{ + class FaultsAgent + { + private static object _locker = new object(); + private bool _forceStop = false; + private ManualResetEvent _manualReset = new ManualResetEvent(false); + + /// + /// Client Hardware key + /// + public string HardwareKey + { + set + { + _hardwareKey = value; + } + } + private string _hardwareKey; + + /// + /// Wake Up + /// + public void WakeUp() + { + _manualReset.Set(); + } + + /// + /// Stops the thread + /// + public void Stop() + { + _forceStop = true; + _manualReset.Set(); + } + + /// + /// Run Thread + /// + public void Run() + { + Trace.WriteLine(new LogMessage("FaultsAgent - Run", "Thread Started"), LogType.Info.ToString()); + + while (!_forceStop) + { + // If we are restarting, reset + _manualReset.Reset(); + + // Reset backOff + int retryAfterSeconds = 0; + lock (_locker) + { + try + { + using (xmds.xmds xmds = new xmds.xmds()) + { + xmds.Credentials = null; + xmds.Url = ApplicationSettings.Default.XiboClient_xmds_xmds + "&method=reportFaults"; + xmds.UseDefaultCredentials = false; + xmds.ReportFaults(ApplicationSettings.Default.ServerKey, _hardwareKey, CacheManager.Instance.UnsafeListAsJsonString()); + } + } + catch (WebException webEx) when (webEx.Response is HttpWebResponse httpWebResponse && (int)httpWebResponse.StatusCode == 429) + { + // Get the header for how long we ought to wait + retryAfterSeconds = webEx.Response.Headers["Retry-After"] != null ? int.Parse(webEx.Response.Headers["Retry-After"]) : 120; + + // Log it. + Trace.WriteLine(new LogMessage("FaultsAgent", "Run: 429 received, waiting for " + retryAfterSeconds + " seconds."), LogType.Info.ToString()); + } + catch (WebException webEx) + { + // Increment the quantity of XMDS failures and bail out + ApplicationSettings.Default.IncrementXmdsErrorCount(); + + // Log this message, but dont abort the thread + Trace.WriteLine(new LogMessage("FaultsAgent - Run", "WebException in Run: " + webEx.Message), LogType.Info.ToString()); + } + catch (Exception ex) + { + // Log this message, but dont abort the thread + Trace.WriteLine(new LogMessage("FaultsAgent - Run", "Exception in Run: " + ex.Message), LogType.Error.ToString()); + } + } + + if (retryAfterSeconds > 0) + { + // Sleep this thread until we've fulfilled our try after + _manualReset.WaitOne(retryAfterSeconds * 1000); + } + else + { + // Sleep this thread until the next collection interval + _manualReset.WaitOne((int)(ApplicationSettings.Default.CollectInterval * ApplicationSettings.Default.XmdsCollectionIntervalFactor() * 1000)); + } + } + + Trace.WriteLine(new LogMessage("FaultsAgent - Run", "Thread Stopped"), LogType.Info.ToString()); + } + } +} diff --git a/app.config b/app.config index 941d39e4..26e535c7 100644 --- a/app.config +++ b/app.config @@ -23,7 +23,7 @@ - http://localhost/xmds.php?v=5 + http://localhost/xmds.php?v=6 From 8a78c0ff9aba7df30f4dc399b79cea830c9284c6 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Tue, 26 Oct 2021 16:14:47 +0100 Subject: [PATCH 11/24] First pass at purge list #229. --- Action/XmrSubscriber.cs | 4 +++ Logic/Schedule.cs | 16 +++++++++- XmdsAgents/LibraryAgent.cs | 47 ++++++++++++++++++++++++----- XmdsAgents/ScheduleAndFilesAgent.cs | 34 +++++++++++++++++++++ 4 files changed, 93 insertions(+), 8 deletions(-) diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index c3ea9f7c..0632f89f 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -263,6 +263,10 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey OnAction?.Invoke(JsonConvert.DeserializeObject(opened)); break; + case "purgeAll": + OnAction?.Invoke(action); + break; + default: Trace.WriteLine(new LogMessage("XmrSubscriber - Run", "Unknown Message: " + action.action), LogType.Info.ToString()); break; diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index e28f242f..dee87f9e 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using System.Windows.Threading; using XiboClient.Action; using XiboClient.Control; using XiboClient.Log; @@ -370,6 +371,10 @@ void _xmrSubscriber_OnAction(Action.PlayerActionInterface action) TriggerWebhookAction webhookAction = (TriggerWebhookAction)action; EmbeddedServerOnTriggerReceived(webhookAction.triggerCode, 0); break; + + case "purgeAll": + _libraryAgent.PurgeAll(); + break; } } @@ -389,9 +394,18 @@ private void _requiredFilesAgent_OnFullyProvisioned() public void wakeUpXmds() { _registerAgent.WakeUp(); - _scheduleAndRfAgent.WakeUp(); _logAgent.WakeUp(); _faultsAgent.WakeUp(); + + // Wake up schedule/rf in 20 seconds to give time for register to complete, which will update our CRC's. + var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(20) }; + timer.Tick += (timerSender, args) => + { + // You only tick once + timer.Stop(); + _scheduleAndRfAgent.WakeUp(); + }; + timer.Start(); } /// diff --git a/XmdsAgents/LibraryAgent.cs b/XmdsAgents/LibraryAgent.cs index 951df006..34e348e9 100644 --- a/XmdsAgents/LibraryAgent.cs +++ b/XmdsAgents/LibraryAgent.cs @@ -48,10 +48,6 @@ public CacheManager CurrentCacheManager } private CacheManager _cacheManager; - /// - /// Required Files Object - /// - private RequiredFiles _requiredFiles; public LibraryAgent() { @@ -66,6 +62,14 @@ public LibraryAgent() _persistentFiles.Add("interrupt.json"); } + /// + /// Wake Up + /// + public void WakeUp() + { + _manualReset.Set(); + } + /// /// Stops the thread /// @@ -102,9 +106,9 @@ public void Run() DateTime testDate = DateTime.Now.AddDays(ApplicationSettings.Default.LibraryAgentInterval * -1); // Get required files from disk - _requiredFiles = RequiredFiles.LoadFromDisk(); + RequiredFiles requiredFiles = RequiredFiles.LoadFromDisk(); - Trace.WriteLine(new LogMessage("LibraryAgent - Run", "Number of required files = " + _requiredFiles.RequiredFileList.Count), LogType.Audit.ToString()); + Trace.WriteLine(new LogMessage("LibraryAgent - Run", "Number of required files = " + requiredFiles.RequiredFileList.Count), LogType.Audit.ToString()); // Build a list of files in the library DirectoryInfo directory = new DirectoryInfo(ApplicationSettings.Default.LibraryPath); @@ -122,7 +126,7 @@ public void Run() // Delete files that were accessed over N days ago try { - RequiredFile file = _requiredFiles.GetRequiredFile(fileInfo.Name); + RequiredFile file = requiredFiles.GetRequiredFile(fileInfo.Name); } catch { @@ -163,5 +167,34 @@ public void Run() Trace.WriteLine(new LogMessage("LibraryAgent - Run", "Thread Stopped"), LogType.Info.ToString()); } + + /// + /// Purge all required files + /// + public void PurgeAll() + { + try + { + // Get required files from disk + foreach (RequiredFile item in RequiredFiles.LoadFromDisk().RequiredFileList) + { + try + { + // Delete and remove from the cache manager + File.Delete(ApplicationSettings.Default.LibraryPath + @"\" + item.SaveAs); + CacheManager.Instance.Remove(item.SaveAs); + } + catch + { + Debug.WriteLine("Unable to process purge item"); + } + } + } + catch (Exception ex) + { + // Log this message, but dont abort the thread + Trace.WriteLine(new LogMessage("LibraryAgent", "PurgeAll: Exception: " + ex.Message), LogType.Error.ToString()); + } + } } } diff --git a/XmdsAgents/ScheduleAndFilesAgent.cs b/XmdsAgents/ScheduleAndFilesAgent.cs index 53a1a5cb..8e36cc73 100644 --- a/XmdsAgents/ScheduleAndFilesAgent.cs +++ b/XmdsAgents/ScheduleAndFilesAgent.cs @@ -218,6 +218,16 @@ public void Run() _requiredFiles = new RequiredFiles(); _requiredFiles.RequiredFilesXml = xml; + // Purge List + try + { + HandlePurgeList(xml); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("ScheduleAndFilesAgent", "Run: exception handling purge list. e: " + e.Message), LogType.Error.ToString()); + } + // List of Threads to start // TODO: Track these threads so that we can abort them if the application closes List threadsToStart = new List(); @@ -484,5 +494,29 @@ private void scheduleAgent() ClientInfo.Instance.ScheduleStatus = "Error. " + ex.Message; } } + + /// + /// Handle the purge list + /// + /// + private void HandlePurgeList(XmlDocument xml) + { + foreach (XmlNode item in xml.SelectNodes("//purge/item")) + { + try + { + // Pull the name from the storedAs attribute + string name = item.Attributes.GetNamedItem("storedAs").Value; + + // Delete and remove from the cache manager + File.Delete(ApplicationSettings.Default.LibraryPath + @"\" + name); + CacheManager.Instance.Remove(name); + } + catch + { + Debug.WriteLine("Unable to process purge item"); + } + } + } } } From 54458067ca90440dea16e10518edbc2b4e9801ab Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 27 Oct 2021 15:51:07 +0100 Subject: [PATCH 12/24] WIP Exchange Manager #231 Added a manager to download content from Adspace, unwrap if necessary and add to an ad buffet. --- Adspace/Ad.cs | 124 +++++++ Adspace/AdspaceNoAdException.cs | 40 +++ Adspace/ExchangeManager.cs | 557 ++++++++++++++++++++++++++++++++ Logic/ApplicationSettings.cs | 1 + Logic/ScheduleItem.cs | 24 ++ Logic/ScheduleManager.cs | 33 ++ XiboClient.csproj | 4 + default.config.xml | 1 + 8 files changed, 784 insertions(+) create mode 100644 Adspace/Ad.cs create mode 100644 Adspace/AdspaceNoAdException.cs create mode 100644 Adspace/ExchangeManager.cs diff --git a/Adspace/Ad.cs b/Adspace/Ad.cs new file mode 100644 index 00000000..1e30282c --- /dev/null +++ b/Adspace/Ad.cs @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2021 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using Flurl; +using Flurl.Http; +using GeoJSON.Net.Contrib.MsSqlSpatial; +using GeoJSON.Net.Feature; +using GeoJSON.Net.Geometry; +using Microsoft.SqlServer.Types; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Device.Location; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Adspace +{ + class Ad + { + public string Id; + public string AdId; + public string Title; + public string CreativeId; + public string Duration; + public string File; + public string Type; + public string XiboType; + public int Width; + public int Height; + + public string Url; + public List ImpressionUrls = new List(); + public List ErrorUrls = new List(); + + public bool IsWrapper; + public int CountWraps = 0; + public string AllowedWrapperType; + public string AllowedWrapperDuration; + + public bool IsGeoAware = false; + public string GeoLocation = ""; + + public double AspectRatio + { + get + { + return (double)Width / Height; + } + } + + /// + /// Download this ad + /// + public void Download() + { + if (!CacheManager.Instance.IsValidPath(File)) + { + // We should download it. + var downloadUrl = new Url(Url); + _ = downloadUrl.DownloadFileAsync(ApplicationSettings.Default.LibraryPath, File).Result; + CacheManager.Instance.Add(File, CacheManager.Instance.GetMD5(File)); + } + } + + /// + /// Set whether or not this GeoSchedule is active. + /// + /// + /// + public bool IsGeoActive(GeoCoordinate geoCoordinate) + { + if (!IsGeoAware) + { + return false; + } + else if (geoCoordinate == null || geoCoordinate.IsUnknown) + { + return false; + } + else + { + try + { + // Current location. + Point current = new Point(new Position(geoCoordinate.Latitude, geoCoordinate.Longitude)); + + // Test against the geo location + var geo = JsonConvert.DeserializeObject(GeoLocation); + + // Use SQL spatial helper to calculate intersection or not + SqlGeometry polygon = (geo.Geometry as Polygon).ToSqlGeometry(); + + return current.ToSqlGeometry().STIntersects(polygon).Value; + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("ScheduleItem", "SetIsGeoActive: Cannot parse geo location: e = " + e.Message), LogType.Audit.ToString()); + } + } + + return false; + } + } +} diff --git a/Adspace/AdspaceNoAdException.cs b/Adspace/AdspaceNoAdException.cs new file mode 100644 index 00000000..cf429464 --- /dev/null +++ b/Adspace/AdspaceNoAdException.cs @@ -0,0 +1,40 @@ +/** + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2021 Xibo Signage Ltd + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Adspace +{ + class AdspaceNoAdException : Exception + { + public AdspaceNoAdException() + { + + } + + public AdspaceNoAdException(string message) : base(message) + { + + } + } +} diff --git a/Adspace/ExchangeManager.cs b/Adspace/ExchangeManager.cs new file mode 100644 index 00000000..4f539a3f --- /dev/null +++ b/Adspace/ExchangeManager.cs @@ -0,0 +1,557 @@ +/** + * Copyright (C) 2021 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +using Flurl; +using Flurl.Http; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using XiboClient.Log; +using XiboClient.Logic; + +namespace XiboClient.Adspace +{ + class ExchangeManager + { +#if DEBUG + private readonly string AdspaceUrl = @"https://test-exchange.xibo-adspace.com/vast/device"; +#else + private readonly string AdspaceUrl = @"https://exchange.xibo-adspace.com/vast/device"; +#endif + + // State + private bool isActive; + private DateTime lastFillDate; + private DateTime lastPrefetchDate; + private List prefetchUrls; + private List adBuffet; + + public int ShareOfVoice { get; private set; } = 0; + public int AverageAdDuration { get; private set; } = 0; + + public ExchangeManager() + { + lastFillDate = DateTime.Now.AddYears(-1); + lastPrefetchDate = DateTime.Now.AddYears(-1); + } + + /// + /// Is an ad available to show? + /// + public bool IsAdAvailable + { + get + { + return CountAvailableAds > 0; + } + } + + /// + /// Count of availabe ads + /// + public int CountAvailableAds + { + get + { + return adBuffet.Count; + } + } + + /// + /// Set adspace as active or inactive + /// + /// + public void SetActive(bool active) + { + if (isActive != active && active) + { + // Transitioning to active + if (prefetchUrls.Count > 0) + { + // TODO: prefetch + } + } + else if (isActive != active) + { + // Transitioning to inactive + adBuffet.Clear(); + } + + // Store the new state + isActive = active; + } + + /// + /// Called to configure the exchange manager for the next set of playbacks + /// + public void Configure() + { + if (IsAdAvailable && ShareOfVoice > 0) + { + Trace.WriteLine(new LogMessage("ExchangeManager", "Configure: we do not need to configure this time around"), LogType.Audit.ToString()); + return; + } + + // No ads available, but we still want to throttle checking for new ones. + if (lastFillDate < DateTime.Now.AddMinutes(-3)) + { + // Fill our ad buffet + Fill(true); + } + + // Should we also prefetch? + if (lastPrefetchDate < DateTime.Now.AddHours(-24)) + { + // TODO: prefetch + } + } + + /// + /// Get an ad for the specified width/height + /// + /// + /// + /// + public Ad GetAd(int width, int height) + { + if (!IsAdAvailable) + { + throw new AdspaceNoAdException(); + } + + Ad ad = adBuffet[0]; + + // Check geo fence + if (!ad.IsGeoActive(ClientInfo.Instance.CurrentGeoLocation)) + { + ReportError(ad.ErrorUrls, 408); + adBuffet.Remove(ad); + throw new AdspaceNoAdException("Outside geofence"); + } + + // Determine type + if (ad.Type.StartsWith("video")) + { + ad.XiboType = "video"; + } + else if (ad.Type.StartsWith("image")) + { + ad.XiboType = "image"; + } + else + { + ReportError(ad.ErrorUrls, 200); + adBuffet.Remove(ad); + throw new AdspaceNoAdException("Type not recognised"); + } + + // Determine Size + if ((double) width / height != ad.AspectRatio) + { + ReportError(ad.ErrorUrls, 203); + adBuffet.Remove(ad); + throw new AdspaceNoAdException("Dimensions invalid"); + } + + // TODO: check fault status + + // Download it (we hope its already there) + ad.Download(); + + // We've converted it into a play + adBuffet.Remove(ad); + + return ad; + } + + /// + /// Prefetch any resources which might play + /// + public async void Prefetch() + { + List urls = new List(); + + foreach (Url url in urls) + { + // Get a JSON string from the URL. + var result = await url.GetJsonAsync>(); + + // Queue each one for download. + // TODO: this needs to be improved. + foreach (string fetchUrl in result) { + string fileName = "axe_" + fetchUrl.Split('/').Last(); + if (!CacheManager.Instance.IsValidPath(fileName)) + { + // We should download it. + var downloadUrl = new Url(fetchUrl); + await downloadUrl.DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName); + CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); + } + } + } + } + + /// + /// Fill the ad buffet + /// + public void Fill() + { + Fill(false); + } + + /// + /// Fill the ad buffet + /// + /// + private void Fill(bool force) + { + lastFillDate = DateTime.Now; + + if (!force && ShareOfVoice > 0 && CountAvailableAds > 0) + { + // We have ads and we aren't forcing a fill + return; + } + + // Make a URL request + var url = new Url(AdspaceUrl); + url = url.AppendPathSegment("request") + .AppendPathSegment(ApplicationSettings.Default.HardwareKey) + .AppendPathSegment(ApplicationSettings.Default.ServerUri); + + if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) + { + url.SetQueryParam("lat", ClientInfo.Instance.CurrentGeoLocation.Latitude) + .SetQueryParam("lng", ClientInfo.Instance.CurrentGeoLocation.Longitude); + } + + adBuffet.AddRange(Request(url)); + } + + /// + /// Request new ads + /// + /// + /// + private List Request(Url url) + { + return Request(url, null); + } + + /// + /// Request new ads + /// + /// + /// If we have a wrapper ad we can resolve it by passing it here. + /// + private List Request(Url url, Ad wrappedAd) + { + List buffet = new List(); + + // Make a request for new ads + try + { + var response = url.GetAsync().Result; + var body = response.GetStringAsync().Result; + + if (string.IsNullOrEmpty(body)) + { + throw new Exception("Empty body"); + } + + // If we are a wrapped ad, then we should attempt to resolve that ad. + // If not, then we are the parent request from adspace and we should resolve the sov/average spot duration/etc + if (wrappedAd == null) + { + string sovHeader = response.Headers.FirstOrDefault(h => h.Name == "x-adspace-sov").Value; + if (!string.IsNullOrEmpty(sovHeader)) + { + try + { + ShareOfVoice = int.Parse(sovHeader); + } + catch + { + Trace.WriteLine(new LogMessage("ExchangeManager", "Request: error parsing SOV header"), LogType.Error.ToString()); + } + } + + string averageAdDurationHeader = response.Headers.FirstOrDefault(h => h.Name == "x-adspace-avg-duration").Value; + if (!string.IsNullOrEmpty(averageAdDurationHeader)) + { + try + { + ShareOfVoice = int.Parse(averageAdDurationHeader); + } + catch + { + Trace.WriteLine(new LogMessage("ExchangeManager", "Request: error parsing avg duration header"), LogType.Error.ToString()); + } + } + } + + // Read the body of the response into XML + XmlDocument document = new XmlDocument(); + document.LoadXml(body); + + // Expect one or more ad nodes. + foreach (XmlNode adNode in document.ChildNodes) + { + // If we have a wrapped ad, resolve it. + Ad ad; + if (wrappedAd == null) + { + ad = new Ad(); + ad.Id = adNode.Attributes["id"].Value; + } + else + { + ad = wrappedAd; + ad.CountWraps++; + } + + // In-line or wrapper + XmlNode wrapper = adNode.SelectSingleNode("./Wrapper"); + if (wrapper != null) + { + ad.IsWrapper = true; + + // Are we wrapped up too much + if (ad.CountWraps >= 5) + { + ReportError(ad.ErrorUrls, 302); + continue; + } + + // pull out the URL we should go to next. + XmlNode adTagUrlNode = wrapper.SelectSingleNode("./VASTAdTagURI"); + if (adTagUrlNode == null) + { + ReportError(ad.ErrorUrls, 302); + continue; + } + + // Make a Url from it. + Url adTagUrl = new Url(adTagUrlNode.Value); + + // Get and impression/error URLs included with this wrap + XmlNode errorUrlNode = wrapper.SelectSingleNode("./Error"); + if (errorUrlNode != null) + { + ad.ErrorUrls.Add(errorUrlNode.Value); + } + + XmlNode impressionUrlNode = wrapper.SelectSingleNode("./Impression"); + if (impressionUrlNode != null) + { + ad.ImpressionUrls.Add(impressionUrlNode.Value); + } + + // Extensions + XmlNodeList extensionNodes = wrapper.SelectNodes("./Extension"); + foreach (XmlNode extensionNode in extensionNodes) + { + switch (extensionNode.Attributes["type"].Value) + { + case "prefetch": + if (prefetchUrls.Contains(extensionNode.InnerText)) + { + prefetchUrls.Add(extensionNode.InnerText); + } + break; + + case "validType": + ad.AllowedWrapperType = extensionNode.InnerText; + break; + + case "validDuration": + ad.AllowedWrapperDuration = extensionNode.InnerText; + break; + } + } + + // Resolve our new wrapper + try + { + buffet.AddRange(Request(adTagUrl, ad)); + } + catch + { + // Ignored + } + finally + { + ad = null; + } + + // If we've handled a wrapper we cannot handle an inline in the same ad. + continue; + } + + // In-line + XmlNode inlineNode = adNode.SelectSingleNode("./InLine"); + if (inlineNode != null) + { + // Title + XmlNode titleNode = inlineNode.SelectSingleNode("./Title"); + if (titleNode != null) + { + ad.Title = titleNode.Value; + } + + // Get and impression/error URLs included with this wrap + XmlNode errorUrlNode = inlineNode.SelectSingleNode("./Error"); + if (errorUrlNode != null) + { + ad.ErrorUrls.Add(errorUrlNode.Value); + } + + XmlNode impressionUrlNode = inlineNode.SelectSingleNode("./Impression"); + if (impressionUrlNode != null) + { + ad.ImpressionUrls.Add(impressionUrlNode.Value); + } + + // Creatives + XmlNode creativeNode = inlineNode.SelectSingleNode("./Creative"); + if (creativeNode != null) + { + ad.CreativeId = creativeNode.Attributes["id"].Value; + + // Get the duration. + XmlNode creativeDurationNode = creativeNode.SelectSingleNode("./Duration"); + if (creativeDurationNode != null) + { + ad.Duration = creativeDurationNode.Value; + } + else + { + ReportError(ad.ErrorUrls, 302); + continue; + } + + // Get the media file + XmlNode creativeMediaNode = creativeNode.SelectSingleNode("./MediaFile"); + if (creativeMediaNode != null) + { + ad.Url = creativeMediaNode.Value; + ad.Width = int.Parse(creativeMediaNode.Attributes["width"].Value); + ad.Height = int.Parse(creativeMediaNode.Attributes["height"].Value); + ad.Type = creativeMediaNode.Attributes["type"].Value; + } + else + { + ReportError(ad.ErrorUrls, 302); + continue; + } + } + else + { + // Malformed Ad. + ReportError(ad.ErrorUrls, 300); + continue; + } + + // Extensions + XmlNodeList extensionNodes = inlineNode.SelectNodes("./Extension"); + foreach (XmlNode extensionNode in extensionNodes) + { + switch (extensionNode.Attributes["type"].Value) + { + case "geoFence": + ad.IsGeoAware = true; + ad.GeoLocation = extensionNode.InnerText; + break; + } + } + + // Did this resolve from a wrapper? if so do some extra checks. + if (ad.IsWrapper) + { + if (!string.IsNullOrEmpty(ad.AllowedWrapperType) + && ad.AllowedWrapperType.ToLower() != "all" + && ad.Type.ToLower() != ad.AllowedWrapperType.ToLower()) + { + ReportError(ad.ErrorUrls, 200); + continue; + } + + if (!string.IsNullOrEmpty(ad.AllowedWrapperDuration) + && ad.Duration != ad.AllowedWrapperDuration) + { + ReportError(ad.ErrorUrls, 302); + continue; + } + } + + // We are good to go. + ad.File = "axe_" + ad.Url.Split('/').Last(); + + // Ad this to our list + buffet.Add(ad); + } + } + + if (buffet.Count <= 0) + { + // Nothing added this time. + throw new Exception("No ads returned this time"); + } + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("ExchangeManager", "Request: failed to make or parse request. e: " + e.Message), LogType.Error.ToString()); + if (wrappedAd != null && wrappedAd.IsWrapper) + { + ReportError(wrappedAd.ErrorUrls, 303); + } + } + + return buffet; + } + + /// + /// Report an error code to a list of URLs + /// + /// + /// + private void ReportError(List urls, int errorCode) + { + foreach (string uri in urls) + { + try + { + var url = new Url(uri.Replace("[ERRORCODE]", "" + errorCode)); + url.WithTimeout(10).GetAsync(); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("ExchangeManager", "ReportError: failed to report error to " + uri + ", code: " + errorCode + ". e: " + e.Message), LogType.Error.ToString()); + } + } + } + } +} diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 73510274..a42cfb1d 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -582,6 +582,7 @@ public bool InDownloadWindow public bool ScreenShotRequested { get; set; } public bool FallbackToInternetExplorer { get; set; } public bool IsRecordGeoLocationOnProofOfPlay { get; set; } + public bool IsAdspaceEnabled { get; set; } // XMDS Status Flags private DateTime _xmdsLastConnection; diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index c4545abe..a711e2fd 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -39,6 +39,11 @@ public class ScheduleItem /// public int ShareOfVoice; + /// + /// Is this schedule item an adspace exchange item + /// + public bool IsAdspaceExchange = false; + /// /// The duration of this event /// @@ -123,6 +128,25 @@ public static ScheduleItem Splash() }; } + /// + /// Create a schedule item for adspace exchange + /// + /// + /// + /// + public static ScheduleItem CreateForAdspaceExchange(int duration, int shareOfVoice) + { + return new ScheduleItem + { + id = -1, + IsAdspaceExchange = true, + ShareOfVoice = shareOfVoice, + Duration = duration, + FromDt = DateTime.MinValue, + ToDt = DateTime.MaxValue + }; + } + /// /// Is this the splash screen? /// diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 6a2a0437..534d2ab6 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -28,6 +28,7 @@ using System.Threading; using System.Xml; using XiboClient.Action; +using XiboClient.Adspace; using XiboClient.Log; using XiboClient.Logic; @@ -69,6 +70,9 @@ class ScheduleManager private bool _refreshSchedule; private DateTime _lastScreenShotDate; + // Adspace Exchange Manager + private ExchangeManager exchangeManager; + /// /// Creates a new schedule Manager /// @@ -91,6 +95,9 @@ public ScheduleManager(string scheduleLocation) // Screenshot _lastScreenShotDate = DateTime.MinValue; + + // Create a new exchange manager + exchangeManager = new ExchangeManager(); } #endregion @@ -416,6 +423,21 @@ private bool IsNewScheduleAvailable() // Load a new overlay schedule List overlaySchedule = LoadNewOverlaySchedule(); + // Load any adspace exchange schedules + if (ApplicationSettings.Default.IsAdspaceEnabled) + { + exchangeManager.SetActive(true); + exchangeManager.Configure(); + if (exchangeManager.ShareOfVoice > 0) + { + parsedSchedule.Add(ScheduleItem.CreateForAdspaceExchange(exchangeManager.AverageAdDuration, exchangeManager.ShareOfVoice)); + } + } + else + { + exchangeManager.SetActive(false); + } + // Do we have any change layout actions? List newSchedule = GetOverrideSchedule(parsedSchedule); if (newSchedule.Count <= 0) @@ -1415,6 +1437,17 @@ public void setAllActionsDownloaded() } } + /// + /// Get an ad from the exchange + /// + /// + /// + /// + public Ad GetAd(int width, int height) + { + return exchangeManager.GetAd(width, height); + } + #endregion } } diff --git a/XiboClient.csproj b/XiboClient.csproj index fa8ab136..09dc0efa 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -115,6 +115,9 @@ + + + @@ -355,6 +358,7 @@ PreserveNewest + echo F|xcopy /Y "$(TargetPath)" "$(TargetDir)\Xibo.scr" diff --git a/default.config.xml b/default.config.xml index 3f8eddde..436955c7 100644 --- a/default.config.xml +++ b/default.config.xml @@ -56,4 +56,5 @@ false individual false + false \ No newline at end of file From 10ae3a5800c2cb5c07e07d7f740c0d4c57c90210 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 28 Oct 2021 16:40:19 +0100 Subject: [PATCH 13/24] WIP Exchange Manager #231 Add schedule, layout parsing, and impression recording. --- Adspace/Ad.cs | 22 ++++++---- Adspace/ExchangeManager.cs | 41 ++++++++++-------- Helpers/Strings.cs | 21 ++++++++++ Logic/ApplicationSettings.cs | 8 ++-- Logic/Schedule.cs | 12 ++++++ Logic/ScheduleManager.cs | 2 +- MainWindow.xaml.cs | 24 ++++++++++- Rendering/Layout.xaml.cs | 81 +++++++++++++++++++++++++++++++----- Rendering/Region.xaml.cs | 17 +++++++- Stats/Stat.cs | 4 ++ Stats/StatManager.cs | 53 ++++++++++++++++++++++- XiboClient.csproj | 1 + 12 files changed, 243 insertions(+), 43 deletions(-) create mode 100644 Helpers/Strings.cs diff --git a/Adspace/Ad.cs b/Adspace/Ad.cs index 1e30282c..9541fbb8 100644 --- a/Adspace/Ad.cs +++ b/Adspace/Ad.cs @@ -29,13 +29,10 @@ using System.Collections.Generic; using System.Device.Location; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace XiboClient.Adspace { - class Ad + public class Ad { public string Id; public string AdId; @@ -68,6 +65,16 @@ public double AspectRatio } } + /// + /// Get the duration in seconds + /// + /// + public int GetDuration() + { + // Duration is a string + return (int)TimeSpan.Parse(Duration).TotalSeconds; + } + /// /// Download this ad /// @@ -76,9 +83,10 @@ public void Download() if (!CacheManager.Instance.IsValidPath(File)) { // We should download it. - var downloadUrl = new Url(Url); - _ = downloadUrl.DownloadFileAsync(ApplicationSettings.Default.LibraryPath, File).Result; - CacheManager.Instance.Add(File, CacheManager.Instance.GetMD5(File)); + new Url(Url).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, File).ContinueWith(t => + { + CacheManager.Instance.Add(File, CacheManager.Instance.GetMD5(File)); + }, System.Threading.Tasks.TaskContinuationOptions.OnlyOnRanToCompletion); } } diff --git a/Adspace/ExchangeManager.cs b/Adspace/ExchangeManager.cs index 4f539a3f..ed9141b6 100644 --- a/Adspace/ExchangeManager.cs +++ b/Adspace/ExchangeManager.cs @@ -46,8 +46,8 @@ class ExchangeManager private bool isActive; private DateTime lastFillDate; private DateTime lastPrefetchDate; - private List prefetchUrls; - private List adBuffet; + private List prefetchUrls = new List(); + private List adBuffet = new List(); public int ShareOfVoice { get; private set; } = 0; public int AverageAdDuration { get; private set; } = 0; @@ -91,7 +91,7 @@ public void SetActive(bool active) // Transitioning to active if (prefetchUrls.Count > 0) { - // TODO: prefetch + Task.Factory.StartNew(() => Prefetch()); } } else if (isActive != active) @@ -125,7 +125,7 @@ public void Configure() // Should we also prefetch? if (lastPrefetchDate < DateTime.Now.AddHours(-24)) { - // TODO: prefetch + Task.Factory.StartNew(() => Prefetch()); } } @@ -135,7 +135,7 @@ public void Configure() /// /// /// - public Ad GetAd(int width, int height) + public Ad GetAd(double width, double height) { if (!IsAdAvailable) { @@ -169,7 +169,7 @@ public Ad GetAd(int width, int height) } // Determine Size - if ((double) width / height != ad.AspectRatio) + if (width / height != ad.AspectRatio) { ReportError(ad.ErrorUrls, 203); adBuffet.Remove(ad); @@ -179,7 +179,7 @@ public Ad GetAd(int width, int height) // TODO: check fault status // Download it (we hope its already there) - ad.Download(); + Task.Factory.StartNew(() => ad.Download()); // We've converted it into a play adBuffet.Remove(ad); @@ -190,25 +190,26 @@ public Ad GetAd(int width, int height) /// /// Prefetch any resources which might play /// - public async void Prefetch() + public void Prefetch() { List urls = new List(); foreach (Url url in urls) { // Get a JSON string from the URL. - var result = await url.GetJsonAsync>(); + var result = url.GetJsonAsync>().Result; - // Queue each one for download. - // TODO: this needs to be improved. + // Download each one foreach (string fetchUrl in result) { string fileName = "axe_" + fetchUrl.Split('/').Last(); if (!CacheManager.Instance.IsValidPath(fileName)) { // We should download it. - var downloadUrl = new Url(fetchUrl); - await downloadUrl.DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName); - CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); + new Url(fetchUrl).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName).ContinueWith(t => + { + CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); + }, + TaskContinuationOptions.OnlyOnRanToCompletion); } } } @@ -240,7 +241,7 @@ private void Fill(bool force) var url = new Url(AdspaceUrl); url = url.AppendPathSegment("request") .AppendPathSegment(ApplicationSettings.Default.HardwareKey) - .AppendPathSegment(ApplicationSettings.Default.ServerUri); + .SetQueryParam("ownerKey", ApplicationSettings.Default.ServerUri); if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) { @@ -304,7 +305,7 @@ private List Request(Url url, Ad wrappedAd) { try { - ShareOfVoice = int.Parse(averageAdDurationHeader); + AverageAdDuration = int.Parse(averageAdDurationHeader); } catch { @@ -318,7 +319,7 @@ private List Request(Url url, Ad wrappedAd) document.LoadXml(body); // Expect one or more ad nodes. - foreach (XmlNode adNode in document.ChildNodes) + foreach (XmlNode adNode in document.DocumentElement.ChildNodes) { // If we have a wrapped ad, resolve it. Ad ad; @@ -545,7 +546,11 @@ private void ReportError(List urls, int errorCode) try { var url = new Url(uri.Replace("[ERRORCODE]", "" + errorCode)); - url.WithTimeout(10).GetAsync(); + url.WithTimeout(10).GetAsync().ContinueWith(t => + { + Trace.WriteLine(new LogMessage("ExchangeManager", "ReportError: failed to report error to " + uri + ", code: " + errorCode), LogType.Error.ToString()); + }, + TaskContinuationOptions.OnlyOnFaulted); } catch (Exception e) { diff --git a/Helpers/Strings.cs b/Helpers/Strings.cs new file mode 100644 index 00000000..1fc1cd77 --- /dev/null +++ b/Helpers/Strings.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XiboClient.Helpers +{ + public static class Strings + { + public static string FirstCharToUpper(this string input) + { + switch (input) + { + case null: throw new ArgumentNullException(nameof(input)); + case "": throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)); + default: return input[0].ToString().ToUpper() + input.Substring(1); + } + } + } +} diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index a42cfb1d..4082dc3f 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -26,6 +26,7 @@ using System.Reflection; using System.Xml; using XiboClient.Action; +using XiboClient.Helpers; using XiboClient.Logic; namespace XiboClient @@ -344,12 +345,13 @@ public void PopulateFromXml(XmlDocument document) // Match these to settings try { - if (lazy.Value[node.Name] != null) + string nodeName = Strings.FirstCharToUpper(node.Name); + if (lazy.Value[nodeName] != null) { - value = Convert.ChangeType(value, lazy.Value[node.Name].GetType()); + value = Convert.ChangeType(value, lazy.Value[nodeName].GetType()); } - lazy.Value[node.Name] = value; + lazy.Value[nodeName] = value; } catch { diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index dee87f9e..c8450905 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Windows.Threading; using XiboClient.Action; +using XiboClient.Adspace; using XiboClient.Control; using XiboClient.Log; using XiboClient.Logic; @@ -706,5 +707,16 @@ public bool NotifyLayoutActionFinished(ScheduleItem item) return false; } + + /// + /// Get an Ad + /// + /// + /// + /// + public Ad GetAd(double width, double height) + { + return _scheduleManager.GetAd(width, height); + } } } diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 534d2ab6..74dd50b6 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -1443,7 +1443,7 @@ public void setAllActionsDownloaded() /// /// /// - public Ad GetAd(int width, int height) + public Ad GetAd(double width, double height) { return exchangeManager.GetAd(width, height); } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 27f2ea88..d187347c 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -31,6 +31,7 @@ using System.Windows.Input; using System.Windows.Media.Imaging; using System.Windows.Threading; +using XiboClient.Adspace; using XiboClient.Error; using XiboClient.Log; using XiboClient.Logic; @@ -691,12 +692,31 @@ private Layout PrepareLayout(ScheduleItem scheduleItem) Height = Height, Schedule = _schedule }; - layout.loadFromFile(scheduleItem); + + // Is this an Adspace Exchange Layout? + if (scheduleItem.IsAdspaceExchange) + { + // Get an ad + Ad ad = _schedule.GetAd(Width, Height); + if (ad == null) + { + throw new LayoutInvalidException("No ad to play"); + } + + layout.LoadFromAd(scheduleItem, ad); + } + else + { + layout.LoadFromFile(scheduleItem); + } return layout; } catch (IOException) { - CacheManager.Instance.Remove(scheduleItem.layoutFile); + if (!scheduleItem.IsAdspaceExchange) + { + CacheManager.Instance.Remove(scheduleItem.layoutFile); + } throw new LayoutInvalidException("IO Exception"); } diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index c4fab558..3b117671 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -29,6 +29,7 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using System.Xml; +using XiboClient.Adspace; using XiboClient.Error; using XiboClient.Logic; using XiboClient.Stats; @@ -112,18 +113,11 @@ public Layout() } /// - /// Load this Layout from its File + /// Load this Layout from its file /// /// - public void loadFromFile(ScheduleItem scheduleItem) + public void LoadFromFile(ScheduleItem scheduleItem) { - // Store the Schedule and LayoutIds - this.ScheduleItem = scheduleItem; - this.ScheduleId = scheduleItem.scheduleid; - this._layoutId = scheduleItem.id; - this.isOverlay = scheduleItem.IsOverlay; - this.isInterrupt = scheduleItem.IsInterrupt(); - // Get this layouts XML XmlDocument layoutXml = new XmlDocument(); @@ -147,7 +141,22 @@ public void loadFromFile(ScheduleItem scheduleItem) throw; } - layoutModifiedTime = File.GetLastWriteTime(scheduleItem.layoutFile); + LoadFromFile(scheduleItem, layoutXml, File.GetLastWriteTime(scheduleItem.layoutFile)); + } + + /// + /// Load this Layout from its XML + /// + /// + public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateTime modifiedDt) + { + // Store the Schedule and LayoutIds + this.ScheduleItem = scheduleItem; + this.ScheduleId = scheduleItem.scheduleid; + this._layoutId = scheduleItem.id; + this.isOverlay = scheduleItem.IsOverlay; + this.isInterrupt = scheduleItem.IsInterrupt(); + layoutModifiedTime = modifiedDt; // Attributes of the main layout node XmlNode layoutNode = layoutXml.SelectSingleNode("/layout"); @@ -401,6 +410,58 @@ public void loadFromFile(ScheduleItem scheduleItem) listMedia = null; } + /// + /// Load this Layout from the Ad provided. + /// + /// + /// + public void LoadFromAd(ScheduleItem scheduleItem, Ad ad) + { + // Create an XLF representing this ad. + XmlDocument document = new XmlDocument(); + XmlElement layout = document.CreateElement("layout"); + XmlElement region = document.CreateElement("region"); + XmlElement media = document.CreateElement("media"); + XmlElement mediaOptions = document.CreateElement("options"); + XmlElement urlOption = document.CreateElement("option"); + + // Layout properties + layout.SetAttribute("width", "" + Width); + layout.SetAttribute("height", "" + Height); + layout.SetAttribute("bgcolor", "#000000"); + layout.SetAttribute("enableStat", "0"); + + // Region properties + region.SetAttribute("id", "axe"); + region.SetAttribute("width", "" + Width); + region.SetAttribute("height", "" + Height); + region.SetAttribute("top", "0"); + region.SetAttribute("left", "0"); + + // Media properties + media.SetAttribute("type", ad.XiboType); + media.SetAttribute("id", Guid.NewGuid().ToString()); + media.SetAttribute("duration", "" + ad.GetDuration()); + media.SetAttribute("enableStat", "0"); + + // Url + urlOption.SetAttribute("name", "uri"); + urlOption.InnerText = ad.File; + + // Add all these nodes to the docs + mediaOptions.AppendChild(urlOption); + media.AppendChild(mediaOptions); + region.AppendChild(media); + layout.AppendChild(region); + document.AppendChild(layout); + + // Pass this XML document to our usual load method + LoadFromFile(scheduleItem, document, DateTime.Now); + + // Set our impression URLs which we will call on stop. + _regions[0].SetAdspaceExchangeImpressionUrls(ad.ImpressionUrls); + } + /// /// Start this Layout /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index ca9bc049..e32e9e1f 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -19,6 +19,7 @@ * along with Xibo. If not, see . */ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Windows; @@ -83,6 +84,11 @@ public partial class Region : UserControl /// private Media navigatedMedia; + /// + /// A list of impression Urls to call on stop. + /// + private List adspaceExchangeImpressionUrls = new List(); + /// /// Track the current sequence /// @@ -653,7 +659,7 @@ private void StopMedia(Media media, bool regionStopped) try { // Close the stat record - StatManager.Instance.WidgetStop(media.ScheduleId, media.LayoutId, media.Id, media.StatsEnabled); + StatManager.Instance.WidgetStop(media.ScheduleId, media.LayoutId, media.Id, media.StatsEnabled, adspaceExchangeImpressionUrls); // Media Stopped Event removes the media from the scene media.MediaStoppedEvent += Media_MediaStoppedEvent; @@ -800,5 +806,14 @@ public void Clear() Trace.WriteLine(new LogMessage("Region - Clear", "Error closing off stat record"), LogType.Error.ToString()); } } + + /// + /// Set any adspace exchange impression urls. + /// + /// + public void SetAdspaceExchangeImpressionUrls(List urls) + { + adspaceExchangeImpressionUrls = urls; + } } } diff --git a/Stats/Stat.cs b/Stats/Stat.cs index 7ba5f3c1..d7cfd7d9 100644 --- a/Stats/Stat.cs +++ b/Stats/Stat.cs @@ -20,6 +20,7 @@ */ using System; using System.Collections.Generic; +using System.Device.Location; namespace XiboClient.Stats { @@ -35,6 +36,9 @@ public class Stat public int Duration; public int Count; + public GeoCoordinate GeoStart; + public GeoCoordinate GeoEnd; + /// /// Engagements (such as geo-location, tags) /// diff --git a/Stats/StatManager.cs b/Stats/StatManager.cs index 8af74c37..2da31ea6 100644 --- a/Stats/StatManager.cs +++ b/Stats/StatManager.cs @@ -18,6 +18,8 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ +using Flurl; +using Flurl.Http; using Microsoft.Data.Sqlite; using Newtonsoft.Json; using System; @@ -298,8 +300,9 @@ public void WidgetStart(int scheduleId, int layoutId, string widgetId) /// /// /// + /// /// Duration - public double WidgetStop(int scheduleId, int layoutId, string widgetId, bool statEnabled) + public double WidgetStop(int scheduleId, int layoutId, string widgetId, bool statEnabled, List urls) { Debug.WriteLine(string.Format("WidgetStop: scheduleId: {0}, layoutId: {1}, widgetId: {2}", scheduleId, layoutId, widgetId), "StatManager"); @@ -332,6 +335,28 @@ public double WidgetStop(int scheduleId, int layoutId, string widgetId, bool sta // Record RecordStat(stat); } + + // Format and save Urls. + if (urls != null) + { + foreach (string url in urls) + { + // We append parameters to the URL and then send or queue + string annotatedUrl = url + "&t=" + ((DateTimeOffset)stat.To).ToUnixTimeMilliseconds(); + + if (stat.GeoEnd != null) + { + annotatedUrl += "&lat=" + stat.GeoEnd.Latitude + "&lng=" + stat.GeoEnd.Longitude; + } + if (stat.GeoStart != null) + { + annotatedUrl += "&latStart=" + stat.GeoStart.Latitude + "&lngStart=" + stat.GeoStart.Longitude; + } + + // Call Impress on a new thread + Task.Factory.StartNew(() => Impress(url)); + } + } } else { @@ -360,6 +385,7 @@ private void AnnotateWithLocation(Stat stat) Count = 1 }; stat.Engagements.Add("LOCATION", engagement); + stat.GeoStart = ClientInfo.Instance.CurrentGeoLocation; } } @@ -377,6 +403,7 @@ private void AnnotateWithLocationUpdate(Stat stat, double duration) if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) { stat.Engagements["LOCATION"].Tag += "|" + ClientInfo.Instance.CurrentGeoLocation.Latitude + "," + ClientInfo.Instance.CurrentGeoLocation.Longitude; + stat.GeoEnd = ClientInfo.Instance.CurrentGeoLocation; } } else @@ -392,6 +419,7 @@ private void AnnotateWithLocationUpdate(Stat stat, double duration) Count = 1 }; stat.Engagements.Add("LOCATION", engagement); + stat.GeoEnd = ClientInfo.Instance.CurrentGeoLocation; } } } @@ -821,5 +849,28 @@ public int TotalReady() return result == null ? 0 : Convert.ToInt32(result); } } + + /// + /// Send an impression, queue on failure + /// + /// + private void Impress(string uri) + { + try + { + // Make a URL + var url = new Url(uri); + _ = url.GetAsync().Result; + } + catch (FlurlHttpTimeoutException) + { + // Queue and resend + // TODO + } + catch + { + Trace.WriteLine(new LogMessage("StatManager", "Impress: unexpected error calling impression url. Url: " + uri), LogType.Error.ToString()); + } + } } } diff --git a/XiboClient.csproj b/XiboClient.csproj index 09dc0efa..07a5a2dc 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -129,6 +129,7 @@ + InfoScreen.xaml From c22680dd4abfe0dc3a6e7e0d23b55e3c5572117a Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 4 Nov 2021 14:12:30 +0000 Subject: [PATCH 14/24] WIP Exchange Manager #231 Ensure download happens after request + second chance to add to cache, 0 duration in schedule manager causes issues, --- Adspace/Ad.cs | 13 +++++-------- Adspace/ExchangeManager.cs | 31 ++++++++++++++++++++----------- Logic/ScheduleItem.cs | 6 ++++-- Logic/ScheduleManager.cs | 6 ++++++ Rendering/Image.cs | 4 +++- Rendering/Layout.xaml.cs | 3 +-- Rendering/Region.xaml.cs | 12 +++++++++++- Stats/StatManager.cs | 2 +- 8 files changed, 51 insertions(+), 26 deletions(-) diff --git a/Adspace/Ad.cs b/Adspace/Ad.cs index 9541fbb8..79c81c08 100644 --- a/Adspace/Ad.cs +++ b/Adspace/Ad.cs @@ -80,14 +80,11 @@ public int GetDuration() /// public void Download() { - if (!CacheManager.Instance.IsValidPath(File)) + // We should download it. + new Url(Url).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, File).ContinueWith(t => { - // We should download it. - new Url(Url).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, File).ContinueWith(t => - { - CacheManager.Instance.Add(File, CacheManager.Instance.GetMD5(File)); - }, System.Threading.Tasks.TaskContinuationOptions.OnlyOnRanToCompletion); - } + CacheManager.Instance.Add(File, CacheManager.Instance.GetMD5(File)); + }, System.Threading.Tasks.TaskContinuationOptions.OnlyOnRanToCompletion); } /// @@ -99,7 +96,7 @@ public bool IsGeoActive(GeoCoordinate geoCoordinate) { if (!IsGeoAware) { - return false; + return true; } else if (geoCoordinate == null || geoCoordinate.IsUnknown) { diff --git a/Adspace/ExchangeManager.cs b/Adspace/ExchangeManager.cs index ed9141b6..7cac0e48 100644 --- a/Adspace/ExchangeManager.cs +++ b/Adspace/ExchangeManager.cs @@ -178,8 +178,11 @@ public Ad GetAd(double width, double height) // TODO: check fault status - // Download it (we hope its already there) - Task.Factory.StartNew(() => ad.Download()); + // Check to see if the file is already there, and if not, download it. + if (!CacheManager.Instance.IsValidPath(ad.File)) + { + Task.Factory.StartNew(() => ad.Download()); + } // We've converted it into a play adBuffet.Remove(ad); @@ -417,36 +420,36 @@ private List Request(Url url, Ad wrappedAd) if (inlineNode != null) { // Title - XmlNode titleNode = inlineNode.SelectSingleNode("./Title"); + XmlNode titleNode = inlineNode.SelectSingleNode("./AdTitle"); if (titleNode != null) { - ad.Title = titleNode.Value; + ad.Title = titleNode.InnerText; } // Get and impression/error URLs included with this wrap XmlNode errorUrlNode = inlineNode.SelectSingleNode("./Error"); if (errorUrlNode != null) { - ad.ErrorUrls.Add(errorUrlNode.Value); + ad.ErrorUrls.Add(errorUrlNode.InnerText); } XmlNode impressionUrlNode = inlineNode.SelectSingleNode("./Impression"); if (impressionUrlNode != null) { - ad.ImpressionUrls.Add(impressionUrlNode.Value); + ad.ImpressionUrls.Add(impressionUrlNode.InnerText); } // Creatives - XmlNode creativeNode = inlineNode.SelectSingleNode("./Creative"); + XmlNode creativeNode = inlineNode.SelectSingleNode("./Creatives/Creative"); if (creativeNode != null) { ad.CreativeId = creativeNode.Attributes["id"].Value; // Get the duration. - XmlNode creativeDurationNode = creativeNode.SelectSingleNode("./Duration"); + XmlNode creativeDurationNode = creativeNode.SelectSingleNode("./Linear/Duration"); if (creativeDurationNode != null) { - ad.Duration = creativeDurationNode.Value; + ad.Duration = creativeDurationNode.InnerText; } else { @@ -455,10 +458,10 @@ private List Request(Url url, Ad wrappedAd) } // Get the media file - XmlNode creativeMediaNode = creativeNode.SelectSingleNode("./MediaFile"); + XmlNode creativeMediaNode = creativeNode.SelectSingleNode("./Linear/MediaFiles/MediaFile"); if (creativeMediaNode != null) { - ad.Url = creativeMediaNode.Value; + ad.Url = creativeMediaNode.InnerText; ad.Width = int.Parse(creativeMediaNode.Attributes["width"].Value); ad.Height = int.Parse(creativeMediaNode.Attributes["height"].Value); ad.Type = creativeMediaNode.Attributes["type"].Value; @@ -511,6 +514,12 @@ private List Request(Url url, Ad wrappedAd) // We are good to go. ad.File = "axe_" + ad.Url.Split('/').Last(); + // Download if necessary + if (!CacheManager.Instance.IsValidPath(ad.File)) + { + Task.Factory.StartNew(() => ad.Download()); + } + // Ad this to our list buffet.Add(ad); } diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index a711e2fd..d8103bda 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -124,7 +124,8 @@ public static ScheduleItem Splash() return new ScheduleItem { id = 0, - scheduleid = 0 + scheduleid = 0, + Duration = 10 }; } @@ -143,7 +144,8 @@ public static ScheduleItem CreateForAdspaceExchange(int duration, int shareOfVoi ShareOfVoice = shareOfVoice, Duration = duration, FromDt = DateTime.MinValue, - ToDt = DateTime.MaxValue + ToDt = DateTime.MaxValue, + layoutFile = "axe" }; } diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 74dd50b6..fb403a66 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -774,6 +774,12 @@ private List ResolveNormalAndInterrupts(List schedul ? CacheManager.Instance.GetLayoutDuration(item.id, 60) : item.Duration; + // Protect against 0 durations + if (duration <= 0) + { + duration = 10; + } + normalSecondsInHour -= duration; resolvedNormal.Add(item); diff --git a/Rendering/Image.cs b/Rendering/Image.cs index 2f1ab933..db9203ad 100644 --- a/Rendering/Image.cs +++ b/Rendering/Image.cs @@ -32,9 +32,11 @@ class Image : Media private string scaleType; private System.Windows.HorizontalAlignment hAlign; private System.Windows.VerticalAlignment vAlign; + private readonly string regionId; public Image(MediaOptions options) : base(options) { + this.regionId = options.regionId; this.filePath = options.uri; this.scaleType = options.Dictionary.Get("scaleType", "stretch"); @@ -91,7 +93,7 @@ public override void RenderMedia(double position) // Set the bitmap as the source of our image this.image = new System.Windows.Controls.Image() { - Name = "Img" + this.Id, + Name = "Img" + this.regionId, Source = bitmap }; diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 3b117671..d7c71e81 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -423,7 +423,7 @@ public void LoadFromAd(ScheduleItem scheduleItem, Ad ad) XmlElement region = document.CreateElement("region"); XmlElement media = document.CreateElement("media"); XmlElement mediaOptions = document.CreateElement("options"); - XmlElement urlOption = document.CreateElement("option"); + XmlElement urlOption = document.CreateElement("uri"); // Layout properties layout.SetAttribute("width", "" + Width); @@ -445,7 +445,6 @@ public void LoadFromAd(ScheduleItem scheduleItem, Ad ad) media.SetAttribute("enableStat", "0"); // Url - urlOption.SetAttribute("name", "uri"); urlOption.InnerText = ad.File; // Add all these nodes to the docs diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index e32e9e1f..a26b53df 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -89,6 +89,11 @@ public partial class Region : UserControl /// private List adspaceExchangeImpressionUrls = new List(); + /// + /// Is this an adspace exchange region? + /// + private bool isAdspaceExchange = false; + /// /// Track the current sequence /// @@ -321,7 +326,11 @@ private void StartNext(double position) if (!SetNextMediaNodeInOptions()) { // For some reason we cannot set a media node... so we need this region to become invalid - CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Region, UnsafeFaultCodes.XlfNoContent, options.layoutId, options.regionId, "Unable to set any region media nodes.", _widgetAvailableTtl); + // we don't do this for adspace exchange, because they all share the same layout. + if (!isAdspaceExchange) + { + CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Region, UnsafeFaultCodes.XlfNoContent, options.layoutId, options.regionId, "Unable to set any region media nodes.", _widgetAvailableTtl); + } // Throw this out so we remove the Layout throw new InvalidOperationException("Unable to set any region media nodes."); @@ -813,6 +822,7 @@ public void Clear() /// public void SetAdspaceExchangeImpressionUrls(List urls) { + isAdspaceExchange = true; adspaceExchangeImpressionUrls = urls; } } diff --git a/Stats/StatManager.cs b/Stats/StatManager.cs index 2da31ea6..3ca737da 100644 --- a/Stats/StatManager.cs +++ b/Stats/StatManager.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * From 326c2b8631776633f366506cca9956f8b504a9dc Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 4 Nov 2021 16:24:30 +0000 Subject: [PATCH 15/24] WIP Exchange Manager #231 Add sensible timeouts and store/forward for impressions. --- Adspace/ExchangeManager.cs | 2 +- Stats/StatManager.cs | 100 ++++++++++++++++++++++++++++++++++++- XmdsAgents/StatAgent.cs | 3 ++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/Adspace/ExchangeManager.cs b/Adspace/ExchangeManager.cs index 7cac0e48..48608ff7 100644 --- a/Adspace/ExchangeManager.cs +++ b/Adspace/ExchangeManager.cs @@ -278,7 +278,7 @@ private List Request(Url url, Ad wrappedAd) // Make a request for new ads try { - var response = url.GetAsync().Result; + var response = url.WithTimeout(10).GetAsync().Result; var body = response.GetStringAsync().Result; if (string.IsNullOrEmpty(body)) diff --git a/Stats/StatManager.cs b/Stats/StatManager.cs index 3ca737da..d7fae42b 100644 --- a/Stats/StatManager.cs +++ b/Stats/StatManager.cs @@ -127,6 +127,16 @@ public void InitDatabase() // Set the DB version to 2 SetDbVersion(connection, 2); } + + // Add the impressions table + if (version <= 2) + { + using (var command = new SqliteCommand("CREATE TABLE IF NOT EXISTS impressions (_id INTEGER PRIMARY KEY, url TEXT, processing INT)", connection)) + { + command.ExecuteNonQuery(); + } + SetDbVersion(connection, 3); + } } } @@ -859,18 +869,104 @@ private void Impress(string uri) try { // Make a URL - var url = new Url(uri); + var url = new Url(uri).WithTimeout(10); _ = url.GetAsync().Result; } catch (FlurlHttpTimeoutException) { // Queue and resend - // TODO + try + { + using (var connection = new SqliteConnection("Filename=" + this.databasePath)) + { + connection.Open(); + + SqliteCommand command = new SqliteCommand + { + Connection = connection, + CommandText = "INSERT INTO impressions (url, processing) VALUES (@url, @processing)" + }; + + command.Parameters.AddWithValue("@url", uri); + command.Parameters.AddWithValue("@processing", 0); + + // Execute and don't wait for the result + command.ExecuteNonQueryAsync().ContinueWith(t => + { + var aggException = t.Exception.Flatten(); + foreach (var exception in aggException.InnerExceptions) + { + Trace.WriteLine(new LogMessage("StatManager", "Impress: Error saving uri to database. Ex = " + exception.Message), LogType.Error.ToString()); + } + }, + TaskContinuationOptions.OnlyOnFaulted); + } + } + catch (Exception ex) + { + Trace.WriteLine(new LogMessage("StatManager", "Impress: Error saving uri to database. Ex = " + ex.Message), LogType.Error.ToString()); + } } catch { Trace.WriteLine(new LogMessage("StatManager", "Impress: unexpected error calling impression url. Url: " + uri), LogType.Error.ToString()); } } + + /// + /// Send any queued impress urls + /// + public void DispatchQueuedImpressUrls(int marker) + { + // Mark records for send. + using (var connection = new SqliteConnection("Filename=" + this.databasePath)) + { + connection.Open(); + + using (SqliteCommand cmd = new SqliteCommand()) + { + cmd.Connection = connection; + cmd.CommandText = "UPDATE impressions SET processing = @processing WHERE _id IN (" + + "SELECT _id FROM stat WHERE ifnull(processing, 0) = 0 ORDER BY _id LIMIT @limit" + + ")"; + + // Set the marker + cmd.Parameters.AddWithValue("@processing", marker); + cmd.Parameters.AddWithValue("@limit", 10); + + int records = cmd.ExecuteNonQuery(); + + if (records <= 0) + { + return; + } + } + + // Select the ones we've marked and process them + using (SqliteCommand cmd = new SqliteCommand()) + { + cmd.Connection = connection; + cmd.CommandText = "SELECT url FROM impressions WHERE processing = @processing"; + cmd.Parameters.AddWithValue("processing", marker); + + using (SqliteDataReader reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + Impress(reader.GetString(0)); + } + } + } + + // Delete them + using (SqliteCommand cmd = new SqliteCommand()) + { + cmd.Connection = connection; + cmd.CommandText = "DELETE FROM impressions WHERE processing = @processing"; + cmd.Parameters.AddWithValue("processing", marker); + cmd.ExecuteScalar(); + } + } + } } } diff --git a/XmdsAgents/StatAgent.cs b/XmdsAgents/StatAgent.cs index f5375cc1..c9691a9a 100644 --- a/XmdsAgents/StatAgent.cs +++ b/XmdsAgents/StatAgent.cs @@ -122,6 +122,9 @@ public void Run() } } + // Process any impression urls + StatManager.Instance.DispatchQueuedImpressUrls(processing); + if (retryAfterSeconds > 0) { // Sleep this thread until we've fulfilled our try after From aa55282b92d6c730e1faf24b689e3df56ad2292c Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 5 Nov 2021 15:47:29 +0000 Subject: [PATCH 16/24] Cycle playback #203 Initial work tracking widget groups. Parse widget groups when creating a layout. --- Log/ClientInfo.cs | 92 +++++++++++++++++++++++++++++++++++++ Logic/XmlHelper.cs | 10 +++++ Rendering/Layout.xaml.cs | 97 +++++++++++++++++++++++++++++++++++++--- Rendering/Region.xaml.cs | 4 +- 4 files changed, 196 insertions(+), 7 deletions(-) diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index 977f0add..dd69729f 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -20,6 +20,7 @@ */ using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.Device.Location; using System.Diagnostics; using System.IO; @@ -104,6 +105,16 @@ private static readonly Lazy /// public GeoCoordinate CurrentGeoLocation { get; set; } + /// + /// Widget Group State + /// + private Dictionary widgetGroupState = new Dictionary(); + + /// + /// Widget Group State Playcount + /// + private Dictionary widgetGroupStatePlaycount = new Dictionary(); + /// /// Client Info Object /// @@ -275,5 +286,86 @@ public long GetDriveFreeSpace() return -1; } } + + #region "Widget Group State" + + /// + /// Get the widget group playcount of the provided group key + /// + /// + /// + public int GetWidgetGroupPlaycount(string groupKey) + { + if (widgetGroupState.ContainsKey(groupKey)) + { + return widgetGroupStatePlaycount[groupKey]; + } + else + { + return 0; + } + } + + /// + /// Get the widget group sequence of the provided group key + /// + /// + /// + public int GetWidgetGroupSequence(string groupKey) + { + if (widgetGroupState.ContainsKey(groupKey)) + { + return widgetGroupState[groupKey]; + } + else + { + return -1; + } + } + + /// + /// Set the widget group sequence + /// + /// + /// + public void SetWidgetGroupSequence(string groupKey, int sequence) + { + if (widgetGroupState.ContainsKey(groupKey)) + { + widgetGroupState[groupKey] = sequence; + } + else + { + widgetGroupState.Add(groupKey, sequence); + } + + // Reset plays to 1. + if (widgetGroupStatePlaycount.ContainsKey(groupKey)) + { + widgetGroupStatePlaycount[groupKey] = 1; + } + else + { + widgetGroupStatePlaycount.Add(groupKey, 1); + } + } + + /// + /// Increment widget group playcount + /// + /// + public void IncrementWidgetGroupPlaycount(string groupKey) + { + if (widgetGroupStatePlaycount.ContainsKey(groupKey)) + { + widgetGroupStatePlaycount[groupKey]++; + } + else + { + widgetGroupStatePlaycount.Add(groupKey, 1); + } + } + + #endregion } } \ No newline at end of file diff --git a/Logic/XmlHelper.cs b/Logic/XmlHelper.cs index 20c5c696..fdcde46d 100644 --- a/Logic/XmlHelper.cs +++ b/Logic/XmlHelper.cs @@ -37,5 +37,15 @@ public static string SelectFirstElementInnerTextOrDefault(XmlDocument doc, strin return (list.Count <= 0) ? defaultValue : list.Item(0).InnerText; } + + public static string GetAttrib(XmlNode node, string attrib, string defVal) + { + XmlAttribute xmlAttrib = node.Attributes[attrib]; + if (xmlAttrib == null) + return defVal; + + string val = xmlAttrib.Value; + return val ?? defVal; + } } } diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index d7c71e81..00202504 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -31,6 +31,7 @@ using System.Xml; using XiboClient.Adspace; using XiboClient.Error; +using XiboClient.Log; using XiboClient.Logic; using XiboClient.Stats; @@ -270,7 +271,7 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT // Get the regions XmlNodeList listRegions = layoutXml.SelectNodes("/layout/region"); - XmlNodeList listMedia = layoutXml.SelectNodes("/layout/region/media"); + int countMedia = layoutXml.SelectNodes("/layout/region/media").Count; _drawer = layoutXml.SelectNodes("/layout/drawer/media"); // Drawer actions @@ -287,10 +288,10 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT } // Check to see if there are any regions on this layout. - if (listRegions.Count == 0 || listMedia.Count == 0) + if (listRegions.Count == 0 || countMedia == 0) { Trace.WriteLine(new LogMessage("PrepareLayout", - string.Format("A layout with {0} regions and {1} media has been detected.", listRegions.Count.ToString(), listMedia.Count.ToString())), + string.Format("A layout with {0} regions and {1} media has been detected.", listRegions.Count.ToString(), countMedia)), LogType.Info.ToString()); // Add this to our unsafe list. @@ -345,7 +346,94 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT options.backgroundTop = options.top * -1; // All the media nodes for this region / layout combination - XmlNodeList mediaNodes = region.SelectNodes("media"); + Dictionary> parsedMedia = new Dictionary> + { + { "flat", new List() } + }; + + // Cycle based playback + // -------------------- + // Are any of this Region's media nodes enabled for cycle playback, and if so, which of the media nodes should we + // add to the node list we provide to the region. + foreach (XmlNode media in region.SelectNodes("media")) + { + bool isCyclePlayback = XmlHelper.GetAttrib(media, "cyclePlayback", "0") == "1"; + + if (isCyclePlayback) + { + string groupKey = XmlHelper.GetAttrib(media, "parentWidgetId", "0"); + + if (!parsedMedia.ContainsKey(groupKey)) + { + parsedMedia.Add(groupKey, new List()); + + // Add the first one of these to retain our place + // this will get swapped out + parsedMedia["flat"].Add(media); + } + parsedMedia[groupKey].Add(media); + } + else + { + parsedMedia["flat"].Add(media); + } + } + + List mediaNodes = new List(); + + // Process the resulting flat list + foreach (XmlNode media in parsedMedia["flat"]) + { + // Is this a cycle based playback node? + bool isCyclePlayback = XmlHelper.GetAttrib(media, "cyclePlayback", "0") == "1"; + + if (isCyclePlayback) + { + // Yes, so replace it with the correct node from our corresponding list + string groupKey = XmlHelper.GetAttrib(media, "parentWidgetId", "0"); + bool isRandom = XmlHelper.GetAttrib(media, "isRandom", "0") == "1"; + int playCount = int.Parse(XmlHelper.GetAttrib(media, "playCount", "1")); + + int sequence = ClientInfo.Instance.GetWidgetGroupSequence(groupKey); + + // If the play count is greater than 1, we need to grab the count plays for the current widget + if (playCount > 1 && ClientInfo.Instance.GetWidgetGroupPlaycount(groupKey) < playCount) + { + // Stick with the current widget + mediaNodes.Add(parsedMedia[groupKey][sequence]); + + // Bump plays + ClientInfo.Instance.IncrementWidgetGroupPlaycount(groupKey); + } + else + { + // Plays of the current widget have been met, so pick a new one. + if (isRandom) + { + // If we are random, then just pick a random number between 0 and the number of widgets + sequence = new Random().Next(0, (parsedMedia[groupKey].Count - 1)); + } + else + { + // Sequential + sequence++; + if (sequence > parsedMedia[groupKey].Count) + { + sequence = 0; + } + } + // Pull out the appropriate widget + mediaNodes.Add(parsedMedia[groupKey][sequence]); + + ClientInfo.Instance.SetWidgetGroupSequence(groupKey, sequence); + } + } + else + { + // Take it as is. + mediaNodes.Add(media); + } + } // Pull out any actions try @@ -407,7 +495,6 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT // Null stuff listRegions = null; - listMedia = null; } /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index a26b53df..3d6648f9 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -62,7 +62,7 @@ public partial class Region : UserControl /// /// The list of media for this Region /// - private XmlNodeList _media; + private List _media; /// /// The Region Options @@ -126,7 +126,7 @@ public Region() ZIndex = 0; } - public void LoadFromOptions(string id, RegionOptions options, XmlNodeList media) + public void LoadFromOptions(string id, RegionOptions options, List media) { // Store dimensions Dimensions = new Rect From b7d04c3e125b2f1434f12ae20725bc0eb33b46cd Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 24 Nov 2021 16:25:08 +0000 Subject: [PATCH 17/24] Cycle playback #203 Initial work tracking Layouts inside a Campaign when cycle is enabled. Initial work selecting the right layout when getting next layout. --- Log/ClientInfo.cs | 93 +++++++++++++++++++++++++++++++++++++++- Logic/Schedule.cs | 36 +++++++++++++++- Logic/ScheduleItem.cs | 9 ++++ Logic/ScheduleManager.cs | 58 +++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 2 deletions(-) diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index dd69729f..0afd098a 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -115,6 +115,16 @@ private static readonly Lazy /// private Dictionary widgetGroupStatePlaycount = new Dictionary(); + /// + /// Widget Group State + /// + private Dictionary campaignGroupState = new Dictionary(); + + /// + /// Widget Group State Playcount + /// + private Dictionary campaignGroupStatePlaycount = new Dictionary(); + /// /// Client Info Object /// @@ -296,7 +306,7 @@ public long GetDriveFreeSpace() /// public int GetWidgetGroupPlaycount(string groupKey) { - if (widgetGroupState.ContainsKey(groupKey)) + if (widgetGroupStatePlaycount.ContainsKey(groupKey)) { return widgetGroupStatePlaycount[groupKey]; } @@ -367,5 +377,86 @@ public void IncrementWidgetGroupPlaycount(string groupKey) } #endregion + + #region Campaign Group state + + /// + /// Get the campaign group playcount of the provided group key + /// + /// + /// + public int GetCampaignGroupPlaycount(string groupKey) + { + if (campaignGroupStatePlaycount.ContainsKey(groupKey)) + { + return campaignGroupStatePlaycount[groupKey]; + } + else + { + return 0; + } + } + + /// + /// Get the campaign group sequence of the provided group key + /// + /// + /// + public int GetCampaignGroupSequence(string groupKey) + { + if (campaignGroupState.ContainsKey(groupKey)) + { + return campaignGroupState[groupKey]; + } + else + { + return -1; + } + } + + /// + /// Set the widget group sequence + /// + /// + /// + public void SetCampaignGroupSequence(string groupKey, int sequence) + { + if (campaignGroupState.ContainsKey(groupKey)) + { + campaignGroupState[groupKey] = sequence; + } + else + { + campaignGroupState.Add(groupKey, sequence); + } + + // Reset plays to 1. + if (campaignGroupStatePlaycount.ContainsKey(groupKey)) + { + campaignGroupStatePlaycount[groupKey] = 1; + } + else + { + campaignGroupStatePlaycount.Add(groupKey, 1); + } + } + + /// + /// Increment widget group playcount + /// + /// + public void IncrementCampaignGroupPlaycount(string groupKey) + { + if (campaignGroupStatePlaycount.ContainsKey(groupKey)) + { + campaignGroupStatePlaycount[groupKey]++; + } + else + { + campaignGroupStatePlaycount.Add(groupKey, 1); + } + } + + #endregion } } \ No newline at end of file diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index c8450905..6dde1225 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -467,7 +467,41 @@ public void NextLayout() ScheduleItem nextLayout = _layoutSchedule[_currentLayout]; - Debug.WriteLine(string.Format("NextLayout: {0}, Interrupt: {1}", nextLayout.layoutFile, nextLayout.IsInterrupt()), "Schedule"); + Debug.WriteLine(string.Format("NextLayout: {0}, Interrupt: {1}, Cycle based: {2}", + nextLayout.layoutFile, + nextLayout.IsInterrupt(), + nextLayout.IsCyclePlayback + ), "Schedule"); + + // If we are cycle playback, then resolve the actual layout we want to play out of the group we have. + if (nextLayout.IsCyclePlayback) + { + // The main layout is sequence 0 + int sequence = ClientInfo.Instance.GetCampaignGroupSequence(nextLayout.CycleGroupKey); + + // Pull out the layout (schedule item) at this group sequence. + if (nextLayout.CyclePlayCount > 1 && ClientInfo.Instance.GetCampaignGroupPlaycount(nextLayout.CycleGroupKey) >= nextLayout.CyclePlayCount) + { + // Next sequence + sequence++; + } + + // Make sure we can get this sequence + if (sequence > nextLayout.CycleScheduleItems.Count) + { + sequence = 0; + } + + // Set the next layout + if (sequence > 0) + { + nextLayout = nextLayout.CycleScheduleItems[sequence]; + } + + // Set the sequence and increment the playcount + ClientInfo.Instance.SetCampaignGroupSequence(nextLayout.CycleGroupKey, sequence); + ClientInfo.Instance.IncrementCampaignGroupPlaycount(nextLayout.CycleGroupKey); + } // Raise the event ScheduleChangeEvent?.Invoke(nextLayout); diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index d8103bda..67f0de0c 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -30,6 +30,9 @@ public class ScheduleItem /// public bool IsOverlay = false; + /// + /// The date/times this item is active from/to + /// public DateTime FromDt; public DateTime ToDt; @@ -54,6 +57,12 @@ public class ScheduleItem public bool IsGeoActive = false; public string GeoLocation = ""; + // Cycle Playback + public bool IsCyclePlayback = false; + public string CycleGroupKey = ""; + public int CyclePlayCount = 0; + public List CycleScheduleItems = new List(); + /// /// Dependent items /// diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index fb403a66..c257d772 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -617,6 +617,52 @@ private List ParseScheduleAndValidate() return resolvedSchedule; } + /// + /// Parse cycle playback out of a schedule + /// + /// + /// + private List ParseCyclePlayback(List schedule) + { + Dictionary> resolved = new Dictionary>(); + resolved.Add("flat", new List()); + foreach (ScheduleItem item in schedule) + { + // Is this item cycle playback enabled? + if (item.IsCyclePlayback) + { + if (!resolved.ContainsKey(item.CycleGroupKey)) + { + // First time we've seen this group key, so add it to the flat list to mark its position. + resolved["flat"].Add(item); + + // Add a new empty list + resolved.Add(item.CycleGroupKey, new List()); + } + + resolved[item.CycleGroupKey].Add(item); + } + else + { + resolved["flat"].Add(item); + } + } + + // Now we go through again and add in + foreach (ScheduleItem item in resolved["flat"]) + { + if (item.IsCyclePlayback) + { + // Pull the relevant list and join in. + // We add an empty one first so that we can use the main item as sequence 0. + item.CycleScheduleItems.Add(new ScheduleItem()); + item.CycleScheduleItems.AddRange(resolved[item.CycleGroupKey]); + } + } + + return resolved["flat"]; + } + /// /// Get Normal Schedule /// @@ -1104,6 +1150,18 @@ private ScheduleItem ParseNodeIntoScheduleItem(XmlNode node) temp.Duration = 0; } } + + // Cycle playback + try + { + temp.IsCyclePlayback = int.Parse(XmlHelper.GetAttrib(node, "cyclePlayback", "0")) == 1; + temp.CycleGroupKey = XmlHelper.GetAttrib(node, "groupKey", ""); + temp.CyclePlayCount = int.Parse(XmlHelper.GetAttrib(node, "playCount", "1")); + } + catch + { + Trace.WriteLine(new LogMessage("ScheduleManager", "ParseNodeIntoScheduleItem: invalid cycle playback configuration."), LogType.Audit.ToString()); + } } // Look for dependents nodes From 828d4f2233dd25ba2fd89b33e6a66d1a2d8fda24 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 24 Nov 2021 16:30:16 +0000 Subject: [PATCH 18/24] Cycle playback #203 Missed parsing the schedule in ParseCyclePlayback --- Logic/ScheduleManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index c257d772..e03f251a 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -443,7 +443,7 @@ private bool IsNewScheduleAvailable() if (newSchedule.Count <= 0) { // No overrides, so we parse in our normal/interrupt layout mix. - newSchedule = ResolveNormalAndInterrupts(parsedSchedule); + newSchedule = ResolveNormalAndInterrupts(ParseCyclePlayback(parsedSchedule)); } // If we have come out of this process without any schedule, then we ought to assign the default From 30b390fe296c4fb076d79508d68d0ea662c09eb4 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 24 Nov 2021 17:03:51 +0000 Subject: [PATCH 19/24] Cycle playback #203 Working prototype for testing. --- Log/ClientInfo.cs | 2 +- Logic/Schedule.cs | 8 +++++++- Logic/ScheduleItem.cs | 3 ++- Logic/ScheduleManager.cs | 3 +++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Log/ClientInfo.cs b/Log/ClientInfo.cs index 0afd098a..9a78ef2a 100644 --- a/Log/ClientInfo.cs +++ b/Log/ClientInfo.cs @@ -410,7 +410,7 @@ public int GetCampaignGroupSequence(string groupKey) } else { - return -1; + return 0; } } diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 6dde1225..23d97bc6 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -233,6 +233,12 @@ private void _scheduleManager_OnNewScheduleAvailable() // Set the current pointer to 0 _currentLayout = 0; + // Record the playback if this is a cycle playback item + if (_layoutSchedule[0].IsCyclePlayback) + { + ClientInfo.Instance.IncrementCampaignGroupPlaycount(_layoutSchedule[0].CycleGroupKey); + } + // Raise a schedule change event ScheduleChangeEvent(_layoutSchedule[0]); @@ -480,7 +486,7 @@ public void NextLayout() int sequence = ClientInfo.Instance.GetCampaignGroupSequence(nextLayout.CycleGroupKey); // Pull out the layout (schedule item) at this group sequence. - if (nextLayout.CyclePlayCount > 1 && ClientInfo.Instance.GetCampaignGroupPlaycount(nextLayout.CycleGroupKey) >= nextLayout.CyclePlayCount) + if (ClientInfo.Instance.GetCampaignGroupPlaycount(nextLayout.CycleGroupKey) >= nextLayout.CyclePlayCount) { // Next sequence sequence++; diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index 67f0de0c..1b29306a 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -154,7 +154,8 @@ public static ScheduleItem CreateForAdspaceExchange(int duration, int shareOfVoi Duration = duration, FromDt = DateTime.MinValue, ToDt = DateTime.MaxValue, - layoutFile = "axe" + layoutFile = "axe", + IsCyclePlayback = false }; } diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index e03f251a..b1334ae7 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -628,6 +628,9 @@ private List ParseCyclePlayback(List schedule) resolved.Add("flat", new List()); foreach (ScheduleItem item in schedule) { + // Clear any existing cycles + item.CycleScheduleItems.Clear(); + // Is this item cycle playback enabled? if (item.IsCyclePlayback) { From 527d6a624cb51733805b7bf572b8895c254fdb3c Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 25 Nov 2021 11:31:21 +0000 Subject: [PATCH 20/24] Fix misc issues with touch action points on layouts with aspect ratio which doesn't match the player window. Fix zindex issue. #237, #238 --- MainWindow.xaml.cs | 2 +- Rendering/Layout.xaml.cs | 36 +++++++++++++++++++++++------------- Rendering/Region.xaml.cs | 15 ++++++++++++++- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index d187347c..4a0d6bf3 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1034,7 +1034,7 @@ public void HandleActionTrigger(string triggerType, string triggerCode, int sour continue; } // If the source of the action is a widget, it must currently be active. - else if (triggerType == "touch" && action.Source == "widget" && currentLayout.GetCurrentWidgetIdForRegion(point) != "" + action.SourceId) + else if (triggerType == "touch" && action.Source == "widget" && !currentLayout.GetCurrentWidgetIdForRegion(point).Contains("" + action.SourceId)) { Debug.WriteLine(point.ToString() + " not active widget: " + action.SourceId, "HandleActionTrigger"); continue; diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 00202504..4525616f 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -345,6 +345,11 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT options.backgroundLeft = options.left * -1; options.backgroundTop = options.top * -1; + // Adjust our left/top to take into account any centering we've done (options left/top are with respect to this layout control, + // which has already been moved). + int actionLeft = options.left + (int)leftOverX; + int actionTop = options.top + (int)leftOverY; + // All the media nodes for this region / layout combination Dictionary> parsedMedia = new Dictionary> { @@ -439,14 +444,14 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT try { // Region Actions - _actions.AddRange(Action.Action.CreateFromXmlNodeList(region.SelectNodes("action"), - options.top, options.left, options.width, options.height)); + _actions.AddRange(Action.Action.CreateFromXmlNodeList(region.SelectNodes("action"), + actionTop, actionLeft, options.width, options.height)); // Widget Actions foreach (XmlNode media in mediaNodes) { - List mediaActions = Action.Action.CreateFromXmlNodeList(media.SelectNodes("action"), - options.top, options.left, options.width, options.height); + List mediaActions = Action.Action.CreateFromXmlNodeList(media.SelectNodes("action"), + actionTop, actionLeft, options.width, options.height); if (mediaActions.Count > 0) { @@ -464,15 +469,19 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT temp.MediaExpiredEvent += Region_MediaExpiredEvent; // ZIndex - if (nodeAttibutes["zindex"] != null) + try + { + temp.ZIndex = int.Parse(XmlHelper.GetAttrib(region, "zindex", "0")); + } + catch { - temp.ZIndex = int.Parse(nodeAttibutes["zindex"].Value); + temp.ZIndex = 0; } Debug.WriteLine("loadFromFile: Created new region", "Layout"); // Load our region - temp.LoadFromOptions(options.regionId, options, mediaNodes); + temp.LoadFromOptions(options.regionId, options, mediaNodes, actionTop, actionLeft); // Add to our list of Regions _regions.Add(temp); @@ -484,7 +493,7 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT _actions.Sort((l, r) => Action.Action.PriorityForActionSource(l.Source) < Action.Action.PriorityForActionSource(r.Source) ? -1 : 1); // Order all Regions by their ZIndex - _regions.Sort((l, r) => l.ZIndex < r.ZIndex ? -1 : 1); + _regions.Sort((l, r) => l.ZIndex <= r.ZIndex ? -1 : 1); // Add all Regions to the Scene foreach (Region temp in _regions) @@ -667,7 +676,7 @@ public void RegionChangeToWidget(string regionId, int widgetId) { if (action.IsDrawer && action.Source == "widget" && action.SourceId == widgetId) { - action.Rect = region.Dimensions; + action.Rect = region.DimensionsForActions; } } } @@ -711,17 +720,18 @@ public bool IsWidgetIdPlaying(string widgetId) /// /// /// - public string GetCurrentWidgetIdForRegion(Point point) + public List GetCurrentWidgetIdForRegion(Point point) { + List activeWidgets = new List(); foreach (Region region in _regions) { - if (region.Dimensions.Contains(point)) + if (region.DimensionsForActions.Contains(point)) { - return region.GetCurrentWidgetId(); + activeWidgets.Add(region.GetCurrentWidgetId()); } } - return null; + return activeWidgets; } /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 3d6648f9..ac61495d 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -54,6 +54,11 @@ public partial class Region : UserControl /// public Rect Dimensions { get; set; } + /// + /// Action Dimensions + /// + public Rect DimensionsForActions { get; set; } + /// /// This Regions zIndex /// @@ -126,7 +131,7 @@ public Region() ZIndex = 0; } - public void LoadFromOptions(string id, RegionOptions options, List media) + public void LoadFromOptions(string id, RegionOptions options, List media, int actionTop, int actionLeft) { // Store dimensions Dimensions = new Rect @@ -136,6 +141,14 @@ public void LoadFromOptions(string id, RegionOptions options, List medi X = options.left, Y = options.top }; + + DimensionsForActions = new Rect + { + Width = options.width, + Height = options.height, + X = actionLeft, + Y = actionTop + }; // Start of by setting our dimensions SetDimensions(options.left, options.top, options.width, options.height); From 5f8011c397e085de5145fe37aab611807f3e3f4f Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 25 Nov 2021 15:34:35 +0000 Subject: [PATCH 21/24] Page Load error trigger logic #236 --- Logic/Schedule.cs | 2 +- Rendering/Layout.xaml.cs | 13 +++++++++++++ Rendering/Media.xaml.cs | 27 +++++++++++++++++++++++++++ Rendering/Region.xaml.cs | 18 ++++++++++++++++++ Rendering/WebCef.cs | 18 ++++++++++++++---- Rendering/WebEdge.cs | 15 ++++++++++++--- Rendering/WebMedia.cs | 8 ++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 23d97bc6..94371e2e 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -706,7 +706,7 @@ public ScheduleItem GetScheduleItemForLayoutCode(string layoutCode) /// /// /// - private void EmbeddedServerOnTriggerReceived(string triggerCode, int sourceId) + public void EmbeddedServerOnTriggerReceived(string triggerCode, int sourceId) { OnTriggerReceived?.Invoke("webhook", triggerCode, sourceId, 0); } diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 4525616f..05bd5dbb 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -467,6 +467,7 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT Region temp = new Region(); temp.DurationElapsedEvent += new Region.DurationElapsedDelegate(Region_DurationElapsedEvent); temp.MediaExpiredEvent += Region_MediaExpiredEvent; + temp.TriggerWebhookEvent += Region_TriggerWebhookEvent; // ZIndex try @@ -605,6 +606,9 @@ public void Remove() { // Clear the region region.Clear(); + region.DurationElapsedEvent -= Region_DurationElapsedEvent; + region.MediaExpiredEvent -= Region_MediaExpiredEvent; + region.TriggerWebhookEvent -= Region_TriggerWebhookEvent; // Remove the region from the list of controls this.LayoutScene.Children.Remove(region); @@ -871,6 +875,15 @@ private void Region_MediaExpiredEvent() Trace.WriteLine(new LogMessage("Region", "MediaExpiredEvent: Media Elapsed"), LogType.Audit.ToString()); } + /// + /// Someone wants to trigger a web hook. + /// + /// + private void Region_TriggerWebhookEvent(string triggerCode, int sourceId) + { + Schedule.EmbeddedServerOnTriggerReceived(triggerCode, sourceId); + } + /// /// Generates a background image and saves it in the library for use later /// diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index 331c7e99..e40e74d3 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -52,6 +52,13 @@ public partial class Media : UserControl public delegate void MediaStoppedDelegate(Media media); public event MediaStoppedDelegate MediaStoppedEvent; + /// + /// Trigger web hook + /// + /// + public delegate void TriggerWebhookDelegate(string triggerCode, int sourceId); + public event TriggerWebhookDelegate TriggerWebhookEvent; + /// /// Have we stopped? /// @@ -909,5 +916,25 @@ public static MediaOptions ParseOptions(XmlNode node) return options; } + + /// + /// Trigger a web hook + /// + /// + /// + protected void TriggerWebhook(string triggerCode) + { + int id; + try + { + id = int.Parse(Id); + } + catch + { + id = 0; + } + + TriggerWebhookEvent?.Invoke(triggerCode, id); + } } } diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index ac61495d..81330f0d 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -120,6 +120,13 @@ public partial class Region : UserControl public delegate void MediaExpiredDelegate(); public event MediaExpiredDelegate MediaExpiredEvent; + /// + /// Event to trigger a webhook + /// + /// + public delegate void TriggerWebhookDelegate(string triggerCode, int sourceId); + public event TriggerWebhookDelegate TriggerWebhookEvent; + /// /// Widget Date WaterMark /// @@ -591,6 +598,7 @@ private Media CreateNextMediaNode(MediaOptions options) // Add event handler for when this completes media.DurationElapsedEvent += new Media.DurationElapsedDelegate(Media_DurationElapsedEvent); + media.TriggerWebhookEvent += Media_TriggerWebhookEvent; // Add event handlers for audio foreach (Media audio in options.Audio) @@ -688,6 +696,7 @@ private void StopMedia(Media media, bool regionStopped) // Tidy Up media.DurationElapsedEvent -= Media_DurationElapsedEvent; + media.TriggerWebhookEvent -= Media_TriggerWebhookEvent; media.Stop(regionStopped); } catch (Exception ex) @@ -787,6 +796,15 @@ private void Media_DurationElapsedEvent(int filesPlayed) } } + /// + /// Trigger web hook event handler. + /// + /// + private void Media_TriggerWebhookEvent(string triggerCode, int sourceId) + { + TriggerWebhookEvent?.Invoke(triggerCode, sourceId); + } + /// /// Set Dimensions /// diff --git a/Rendering/WebCef.cs b/Rendering/WebCef.cs index 25e922af..76773e75 100644 --- a/Rendering/WebCef.cs +++ b/Rendering/WebCef.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -180,11 +180,21 @@ private void WebView_LoadError(object sender, CefSharp.LoadErrorEventArgs e) } else { - Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ErrorText + ", code = " + e.ErrorCode), LogType.Error.ToString()); - - // This should expire the media + // This should expire the media in a short while Duration = 5; base.RestartTimer(); + + // If we have a trigger to use, then fire it off (we will still expire if this isn't handled) + if (!string.IsNullOrEmpty(PageLoadErrorTrigger)) + { + // Fire off the page load error trigger. + TriggerWebhook(PageLoadErrorTrigger); + } + else + { + // Unexpected, so log. + Trace.WriteLine(new LogMessage("WebCef", "WebView_LoadError: Cannot navigate. e = " + e.ErrorText + ", code = " + e.ErrorCode), LogType.Error.ToString()); + } } } diff --git a/Rendering/WebEdge.cs b/Rendering/WebEdge.cs index ec6badae..de36a488 100644 --- a/Rendering/WebEdge.cs +++ b/Rendering/WebEdge.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -215,11 +215,20 @@ private void WebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.C } else { - Trace.WriteLine(new LogMessage("WebView", "WebView_NavigationCompleted: e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); - // This should exipre the media Duration = 5; base.RestartTimer(); + + // If we have a trigger to use, then fire it off (we will still expire if this isn't handled) + if (!string.IsNullOrEmpty(PageLoadErrorTrigger)) + { + // Fire off the page load error trigger. + TriggerWebhook(PageLoadErrorTrigger); + } + else + { + Trace.WriteLine(new LogMessage("WebView", "WebView_NavigationCompleted: e = " + e.WebErrorStatus.ToString()), LogType.Error.ToString()); + } } } diff --git a/Rendering/WebMedia.cs b/Rendering/WebMedia.cs index d6efff2e..e2fda399 100644 --- a/Rendering/WebMedia.cs +++ b/Rendering/WebMedia.cs @@ -48,6 +48,11 @@ abstract class WebMedia : Media /// private bool _reloadOnXmdsRefresh = false; + /// + /// A string to trigger on page load error + /// + protected string PageLoadErrorTrigger; + // Events public delegate void HtmlUpdatedDelegate(string url); public event HtmlUpdatedDelegate HtmlUpdatedEvent; @@ -71,6 +76,9 @@ public WebMedia(MediaOptions options) { // If we are modeid == 1, then just open the webpage without adjusting the file path _filePath = Uri.UnescapeDataString(options.uri).Replace('+', ' '); + + // Do we have a page load error trigger? + PageLoadErrorTrigger = options.Dictionary.Get("pageLoadErrorTrigger"); } else { From 0aba92c3bb892f6bb74fd814eacb9db1f774a0da Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 25 Nov 2021 15:56:30 +0000 Subject: [PATCH 22/24] Bump version for a beta build. --- Logic/ApplicationSettings.cs | 2 +- Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 4082dc3f..2c355bbb 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -52,7 +52,7 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R302.2"; + public string ClientVersion { get; } = "3 R302.3"; public string Version { get; } = "6"; public int ClientCodeVersion { get; } = 302; diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index eb617008..243bcc7e 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.302.2.0")] -[assembly: AssemblyFileVersion("3.302.2.0")] +[assembly: AssemblyVersion("3.302.3.0")] +[assembly: AssemblyFileVersion("3.302.3.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] From 078f5a8e6cbd2a269bb141a6f0603deb6a1349c9 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 12 Jan 2022 14:41:28 +0000 Subject: [PATCH 23/24] Widget: Don't overwrite 0 duration for audio #241 --- Rendering/Media.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index e40e74d3..e54d8f18 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -763,7 +763,7 @@ public static MediaOptions ParseOptions(XmlNode node) } // We cannot have a 0 duration here... not sure why we would... but - if (options.duration == 0 && options.type != "video" && options.type != "localvideo") + if (options.duration == 0 && options.type != "video" && options.type != "localvideo" && options.type != "audio") { int emptyLayoutDuration = int.Parse(ApplicationSettings.Default.EmptyLayoutDuration.ToString()); options.duration = (emptyLayoutDuration == 0) ? 10 : emptyLayoutDuration; From aa769753bcdcde9b83570ccadae61de33de12e37 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 12 Jan 2022 15:00:56 +0000 Subject: [PATCH 24/24] Bump to 302.4. Update libraries. --- Logic/ApplicationSettings.cs | 2 +- Properties/AssemblyInfo.cs | 4 ++-- Resources/licence.txt | 2 +- XiboClient.csproj | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 2c355bbb..577c6ce3 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -52,7 +52,7 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R302.3"; + public string ClientVersion { get; } = "3 R302.4"; public string Version { get; } = "6"; public int ClientCodeVersion { get; } = 302; diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 243bcc7e..570156d0 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.302.3.0")] -[assembly: AssemblyFileVersion("3.302.3.0")] +[assembly: AssemblyVersion("3.302.4.0")] +[assembly: AssemblyFileVersion("3.302.4.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Resources/licence.txt b/Resources/licence.txt index 46343978..d4020cc2 100644 --- a/Resources/licence.txt +++ b/Resources/licence.txt @@ -1,5 +1,5 @@ Xibo - Digital Signage - http://www.xibo.org.uk -Copyright (C) 2020 Xibo Signage Ltd +Copyright (C) 2022 Xibo Signage Ltd Xibo is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/XiboClient.csproj b/XiboClient.csproj index 07a5a2dc..2bea0500 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -297,13 +297,13 @@ 1.8.9 - 94.4.50 + 96.0.180 1.2.0 - 1.15.0 + 1.16.0 3.4.3 @@ -321,13 +321,13 @@ 0.3.6 - 5.0.6 + 6.0.1 14.0.1016.290 - 1.0.992.28 + 1.0.1072.54 4.0.1.6 @@ -336,10 +336,10 @@ 13.0.1 - 3.0.5 + 3.0.9 - 3.1.0 + 5.0.1 4.7.0