From be77f3b783fd69fbc20e2ae7cf9ef109a9581f5a Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 9 Mar 2022 09:44:22 +0000 Subject: [PATCH 1/6] Bump version to R303 and update deps --- Logic/ApplicationSettings.cs | 6 +++--- Properties/AssemblyInfo.cs | 6 +++--- XiboClient.csproj | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 577c6ce..cca0c3b 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -52,9 +52,9 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R302.4"; + public string ClientVersion { get; } = "3 R303.0"; public string Version { get; } = "6"; - public int ClientCodeVersion { get; } = 302; + public int ClientCodeVersion { get; } = 303; private ApplicationSettings() { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 570156d..4aa239d 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Xibo Digital Signage")] [assembly: AssemblyProduct("Xibo")] -[assembly: AssemblyCopyright("Copyright © Xibo Signage Ltd 2021")] +[assembly: AssemblyCopyright("Copyright © Xibo Signage Ltd 2022")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -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.4.0")] -[assembly: AssemblyFileVersion("3.302.4.0")] +[assembly: AssemblyVersion("3.303.0.0")] +[assembly: AssemblyFileVersion("3.303.0.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/XiboClient.csproj b/XiboClient.csproj index 2bea050..bda91f9 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -297,7 +297,7 @@ 1.8.9 - 96.0.180 + 99.2.90 1.2.0 @@ -309,10 +309,10 @@ 3.4.3 - 3.0.2 + 3.0.4 - 3.2.0 + 3.2.2 1.2.19 @@ -321,16 +321,16 @@ 0.3.6 - 6.0.1 + 6.0.3 14.0.1016.290 - 1.0.1072.54 + 1.0.1108.44 - 4.0.1.6 + 4.0.1.8 13.0.1 From a8d8b14822ae025993309aa1eb8388e9b0cf0a4a Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 9 Mar 2022 10:50:17 +0000 Subject: [PATCH 2/6] XMR: improve collect now XMR: improve socket disposal #247 --- Action/XmrSubscriber.cs | 37 ++++++++++++++++--------------------- Logic/Schedule.cs | 33 +++++++++++++++++++++++++++++---- XmdsAgents/RegisterAgent.cs | 15 +++++++++++++++ 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Action/XmrSubscriber.cs b/Action/XmrSubscriber.cs index 0632f89..65231db 100644 --- a/Action/XmrSubscriber.cs +++ b/Action/XmrSubscriber.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -41,8 +41,9 @@ class XmrSubscriber /// /// Last Heartbeat packet received + /// Assume a successful connection so that a check doesn't immediately tear down the socket. /// - public DateTime LastHeartBeat = DateTime.MinValue; + public DateTime LastHeartBeat = DateTime.Now; // Events public delegate void OnActionDelegate(PlayerActionInterface action); @@ -65,11 +66,6 @@ public HardwareKey HardwareKey /// private NetMQPoller _poller; - /// - /// The Init Address - /// - private string _address; - /// /// Runs the agent /// @@ -89,9 +85,6 @@ public void Run() // Check we have an address to connect to. if (!string.IsNullOrEmpty(ApplicationSettings.Default.XmrNetworkAddress) && ApplicationSettings.Default.XmrNetworkAddress != "DISABLED") { - // Cache the address for this socket (the setting may change outside). - _address = ApplicationSettings.Default.XmrNetworkAddress; - // Get the Private Key AsymmetricCipherKeyPair rsaKey = _hardwareKey.getXmrKey(); @@ -278,21 +271,22 @@ private void processMessage(NetMQMessage message, AsymmetricCipherKeyPair rsaKey /// public void Restart() { + // Stop the poller try { - // Stop the poller if (_poller != null) { _poller.Stop(); + _poller.Dispose(); } - - // Wakeup - _manualReset.Set(); } catch (Exception e) { - Trace.WriteLine(new LogMessage("XmrSubscriber - Restart", "Unable to Restart XMR: " + e.Message), LogType.Info.ToString()); + Trace.WriteLine(new LogMessage("XmrSubscriber - Restart", "Unable to stop XMR during restart: " + e.Message), LogType.Info.ToString()); } + + // Wakeup + _manualReset.Set(); } /// @@ -306,18 +300,19 @@ public void Stop() if (_poller != null) { _poller.Stop(); + _poller.Dispose(); } - - // Stop the thread at the next loop - _forceStop = true; - - // Wakeup - _manualReset.Set(); } catch (Exception e) { Trace.WriteLine(new LogMessage("XmrSubscriber - Stop", "Unable to Stop XMR: " + e.Message), LogType.Info.ToString()); } + + // Stop the thread at the next loop + _forceStop = true; + + // Wakeup + _manualReset.Set(); } } } diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 94371e2..a303f6b 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -63,6 +63,11 @@ public class Schedule /// private bool _stopCalled = false; + /// + /// Should we trigger a schedule call on register complete? + /// + private bool _triggerScheduleOnRegisterComplete = false; + #region Threads and Agents // Key private HardwareKey _hardwareKey; @@ -117,6 +122,7 @@ public Schedule(string scheduleLocation) // Create a Register Agent _registerAgent = new RegisterAgent(); _registerAgent.OnXmrReconfigure += _registerAgent_OnXmrReconfigure; + _registerAgent.OnRegisterComplete += _registerAgent_OnRegisterComplete; _registerAgentThread = new Thread(new ThreadStart(_registerAgent.Run)); _registerAgentThread.Name = "RegisterAgentThread"; @@ -312,6 +318,21 @@ void _registerAgent_OnXmrReconfigure() restartXmr(); } + /// + /// Regiser call has completed. + /// + /// + private void _registerAgent_OnRegisterComplete(bool error) + { + if (!error && _triggerScheduleOnRegisterComplete) + { + _scheduleAndRfAgent.WakeUp(); + } + + // Reset + _triggerScheduleOnRegisterComplete = false; + } + /// /// XMR Subscriber Action /// @@ -400,17 +421,20 @@ private void _requiredFilesAgent_OnFullyProvisioned() /// public void wakeUpXmds() { + // Wake up schedule/rf after the register call, which will update our CRC's. + _triggerScheduleOnRegisterComplete = true; _registerAgent.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. + // Wake up other calls in a little while (give the rest time to complete so we send the latest info) var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(20) }; timer.Tick += (timerSender, args) => { // You only tick once timer.Stop(); - _scheduleAndRfAgent.WakeUp(); + + // Wake + _logAgent.WakeUp(); + _faultsAgent.WakeUp(); }; timer.Start(); } @@ -631,6 +655,7 @@ public void Stop() _stopCalled = true; // Stop the register agent + _registerAgent.OnRegisterComplete -= _registerAgent_OnRegisterComplete; _registerAgent.Stop(); // Stop the requiredfiles agent diff --git a/XmdsAgents/RegisterAgent.cs b/XmdsAgents/RegisterAgent.cs index 60c950a..4d5a459 100644 --- a/XmdsAgents/RegisterAgent.cs +++ b/XmdsAgents/RegisterAgent.cs @@ -42,6 +42,9 @@ class RegisterAgent public delegate void OnXmrReconfigureDelegate(); public event OnXmrReconfigureDelegate OnXmrReconfigure; + public delegate void OnRegisterCompleteDelegate(bool error); + public event OnRegisterCompleteDelegate OnRegisterComplete; + /// /// Wake Up /// @@ -181,6 +184,9 @@ public void Run() } } } + + // Complete + OnRegisterComplete?.Invoke(false); } catch (WebException webEx) when (webEx.Response is HttpWebResponse httpWebResponse && (int)httpWebResponse.StatusCode == 429) { @@ -189,6 +195,9 @@ public void Run() // Log it. Trace.WriteLine(new LogMessage("LogAgent", "Run: 429 received, waiting for " + retryAfterSeconds + " seconds."), LogType.Info.ToString()); + + // Complete, failed + OnRegisterComplete?.Invoke(true); } catch (WebException webEx) { @@ -197,11 +206,17 @@ public void Run() // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("RegisterAgent - Run", "WebException in Run: " + webEx.Message), LogType.Info.ToString()); + + // Complete, failed + OnRegisterComplete?.Invoke(true); } catch (Exception ex) { // Log this message, but dont abort the thread Trace.WriteLine(new LogMessage("RegisterAgent - Run", "Exception in Run: " + ex.Message), LogType.Info.ToString()); + + // Complete, failed + OnRegisterComplete?.Invoke(true); } } From bc11489d79e960759ca38754db2a14e0715a7085 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 9 Mar 2022 10:58:45 +0000 Subject: [PATCH 3/6] Widgets: image scale type fit #244 --- Rendering/Image.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Rendering/Image.cs b/Rendering/Image.cs index db9203a..ff4bee1 100644 --- a/Rendering/Image.cs +++ b/Rendering/Image.cs @@ -1,5 +1,5 @@ /** -* Copyright (C) 2020 Xibo Signage Ltd +* Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -102,15 +102,19 @@ public override void RenderMedia(double position) { this.image.Stretch = System.Windows.Media.Stretch.Fill; } + else if (this.scaleType == "fit") + { + this.image.Stretch = System.Windows.Media.Stretch.UniformToFill; + } else { this.image.Stretch = System.Windows.Media.Stretch.Uniform; - - // Further worry about alignment - this.image.HorizontalAlignment = this.hAlign; - this.image.VerticalAlignment = this.vAlign; } + // Further worry about alignment + this.image.HorizontalAlignment = this.hAlign; + this.image.VerticalAlignment = this.vAlign; + this.MediaScene.Children.Add(this.image); // Call base render to set off timers, etc. From 5456127e5154454ce4c4c88d9aafbd64a2242c4f Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 9 Mar 2022 17:16:17 +0000 Subject: [PATCH 4/6] Transitions: use region exit transition if one exists (otherwise use widget) #245 To do this we need to delay removing regions/layouts from the control set until those transitions have completed. I've added events to facilitate this. --- Logic/RegionOptions.cs | 5 +++ MainWindow.xaml.cs | 20 ++++++++-- Rendering/Layout.xaml.cs | 85 ++++++++++++++++++++++++++++++++++++---- Rendering/Media.xaml.cs | 19 +++++++-- Rendering/Region.xaml.cs | 64 ++++++++++++++++++++---------- 5 files changed, 158 insertions(+), 35 deletions(-) diff --git a/Logic/RegionOptions.cs b/Logic/RegionOptions.cs index f17184c..e0c828e 100644 --- a/Logic/RegionOptions.cs +++ b/Logic/RegionOptions.cs @@ -59,6 +59,11 @@ public struct RegionOptions public int PlayerWidth { get; set; } public int PlayerHeight { get; set; } + // Region out transition + public string TransitionType; + public int TransitionDuration; + public string TransitionDirection; + public override string ToString() { return string.Format("({0},{1},{2},{3},{4},{5})", width, height, top, left, layoutId, regionId); diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 4a0d6bf..e639e3e 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -480,7 +480,7 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) try { - // Destroy the Current Layout + // Stop the Current Layout try { if (this.currentLayout != null) @@ -499,8 +499,6 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) this.currentLayout.Stop(); - DestroyLayout(this.currentLayout); - Debug.WriteLine("ChangeToNextLayout: stopped and removed the current Layout", "MainWindow"); } } @@ -602,6 +600,9 @@ private void StartLayout(Layout layout) { Debug.WriteLine("StartLayout: Starting...", "MainWindow"); + // Bind to Layout finished + layout.OnLayoutStopped += Layout_OnLayoutStopped; + // Match Background Colors this.Background = layout.BackgroundColor; @@ -793,14 +794,25 @@ private void RemoveSplashScreen() this._showingSplash = false; } + /// + /// Event called when a Layout has been stopped + /// + private void Layout_OnLayoutStopped(Layout layout) + { + Debug.WriteLine("Layout_OnLayoutStopped: Layout completely stopped", "MainWindow"); + + DestroyLayout(layout); + } + /// /// Disposes Layout - removes the controls /// private void DestroyLayout(Layout layout) { - Debug.WriteLine("DestoryLayout: Destroying Layout", "MainForm"); + Debug.WriteLine("DestroyLayout: Destroying Layout", "MainWindow"); layout.Remove(); + layout.OnLayoutStopped -= Layout_OnLayoutStopped; this.Scene.Children.Remove(layout); } diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 05bd5db..8578bd7 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -99,6 +99,12 @@ public partial class Layout : UserControl public bool IsRunning { get; set; } public bool IsExpired { get; private set; } + /// + /// Event to indicate that a Layout has stopped. + /// + public delegate void OnLayoutStoppedDelegate(Layout layout); + public event OnLayoutStoppedDelegate OnLayoutStopped; + /// /// Layout /// @@ -310,7 +316,7 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT continue; } - // Region loop setting + // Region options: loop, transitions. options.RegionLoop = false; XmlNode regionOptionsNode = region.SelectSingleNode("options"); @@ -320,11 +326,32 @@ public void LoadFromFile(ScheduleItem scheduleItem, XmlDocument layoutXml, DateT foreach (XmlNode option in regionOptionsNode.ChildNodes) { if (option.Name == "loop" && option.InnerText == "1") + { options.RegionLoop = true; + } + else if (option.Name == "transitionType") + { + options.TransitionType = option.InnerText; + } + else if (option.Name == "transitionDuration" && !string.IsNullOrEmpty(option.InnerText)) + { + try + { + options.TransitionDuration = int.Parse(option.InnerText); + } + catch + { + options.TransitionDuration = 2000; + } + } + else if (option.Name == "transitionDirection") + { + options.TransitionDirection = option.InnerText; + } } } - //each region + // Each region XmlAttributeCollection nodeAttibutes = region.Attributes; options.scheduleId = ScheduleId; @@ -467,6 +494,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.OnRegionStopped += Region_OnRegionStopped; temp.TriggerWebhookEvent += Region_TriggerWebhookEvent; // ZIndex @@ -587,6 +615,25 @@ public void Stop() // Record final duration of this layout in memory cache CacheManager.Instance.RecordLayoutDuration(_layoutId, (int)Math.Ceiling(duration)); + // Stop each region and let their transitions play out (if any) + lock (_regions) + { + foreach (Region region in _regions) + { + try + { + region.Stop(); + } + catch (Exception e) + { + // If we can't dispose we should log to understand why + Trace.WriteLine(new LogMessage("Layout", "Remove: " + e.Message), LogType.Info.ToString()); + + this.LayoutScene.Children.Remove(region); + } + } + } + IsRunning = false; } @@ -605,12 +652,11 @@ public void Remove() try { // Clear the region - region.Clear(); region.DurationElapsedEvent -= Region_DurationElapsedEvent; region.MediaExpiredEvent -= Region_MediaExpiredEvent; + region.OnRegionStopped -= Region_OnRegionStopped; region.TriggerWebhookEvent -= Region_TriggerWebhookEvent; - // Remove the region from the list of controls this.LayoutScene.Children.Remove(region); } catch (Exception e) @@ -762,8 +808,8 @@ public void ExecuteWidget(int widgetId) // Execute this media node immediately. media.RenderMedia(0); - // Stop it - media.Stop(true); + // Stop it (no transition) + media.Stop(false); media = null; })); } @@ -875,6 +921,31 @@ private void Region_MediaExpiredEvent() Trace.WriteLine(new LogMessage("Region", "MediaExpiredEvent: Media Elapsed"), LogType.Audit.ToString()); } + /// + /// A region has stopped. + /// + private void Region_OnRegionStopped() + { + Debug.WriteLine("Region_OnRegionStopped: Region stopped", "Layout"); + + foreach (Region temp in _regions) + { + if (!temp.IsStopped) + { + return; + } + } + + Debug.WriteLine("Region_OnRegionStopped: All regions stopped", "Layout"); + + // All regions have stopped. + // Yield and then call stop. + Dispatcher.BeginInvoke(new System.Action(() => + { + OnLayoutStopped?.Invoke(this); + })); + } + /// /// Someone wants to trigger a web hook. /// diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index e54d8f1..bf19862 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -257,11 +257,11 @@ public void SignalElapsedEvent() /// /// Stop this Media - /// + /// /// - public void Stop(bool regionStopped) + public void Stop(bool isShouldTransition) { - if (regionStopped) + if (!isShouldTransition) { this._stopped = true; this.MediaStoppedEvent?.Invoke(this); @@ -375,6 +375,19 @@ public void TransitionOut() } } + /// + /// Override the out transition with a new one + /// + /// + /// + /// + public void OverrideTransitionOut(string type, int duration, string direction) + { + this.options.Dictionary.Replace("transOut", type); + this.options.Dictionary.Replace("transOutDuration", "" + duration); + this.options.Dictionary.Replace("transOutDirection", direction); + } + /// /// Animation completed /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 81330f0..b2d56fe 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -49,6 +49,11 @@ public partial class Region : UserControl /// public bool IsExpired = false; + /// + /// Is stopped? + /// + public bool IsStopped = false; + /// /// Region Dimensions /// @@ -120,6 +125,12 @@ public partial class Region : UserControl public delegate void MediaExpiredDelegate(); public event MediaExpiredDelegate MediaExpiredEvent; + /// + /// Event to indicate that a Region has stopped. + /// + public delegate void OnRegionStoppedDelegate(); + public event OnRegionStoppedDelegate OnRegionStopped; + /// /// Event to trigger a webhook /// @@ -188,6 +199,7 @@ public string GetCurrentWidgetId() public void Start() { // Start this region + IsStopped = false; this.currentSequence = -1; StartNext(0); } @@ -474,7 +486,7 @@ private bool SetNextMediaNodeInOptions() { try { - // Unbind any events and dispose + // Unbind any events and dispose, no transition audio.DurationElapsedEvent -= Audio_DurationElapsedEvent; audio.Stop(false); } @@ -673,17 +685,7 @@ private void Audio_DurationElapsedEvent(int filesPlayed) /// private void StopMedia(Media media) { - StopMedia(media, false); - } - - /// - /// Stop normal media node - /// - /// - /// - private void StopMedia(Media media, bool regionStopped) - { - Trace.WriteLine(new LogMessage("Region", "StopMedia: " + media.Id + " stopping, region stopped " + regionStopped), LogType.Audit.ToString()); + Trace.WriteLine(new LogMessage("Region", "StopMedia: " + media.Id + " stopping, region stopped " + IsStopped), LogType.Audit.ToString()); // Dispose of the current media try @@ -697,7 +699,16 @@ private void StopMedia(Media media, bool regionStopped) // Tidy Up media.DurationElapsedEvent -= Media_DurationElapsedEvent; media.TriggerWebhookEvent -= Media_TriggerWebhookEvent; - media.Stop(regionStopped); + + // Should we override the transition if we have one? + if (IsStopped && !string.IsNullOrEmpty(options.TransitionType)) + { + // This region is stopped, so provide the opportunity for a region exit transition instead of a media transition. + media.OverrideTransitionOut(options.TransitionType, options.TransitionDuration, options.TransitionDirection); + } + + // Stop with transition. + media.Stop(true); } catch (Exception ex) { @@ -705,6 +716,9 @@ private void StopMedia(Media media, bool regionStopped) // Remove the controls RegionScene.Children.Remove(media); + + // We are stopped + OnRegionStopped?.Invoke(); } } @@ -721,6 +735,12 @@ private void Media_MediaStoppedEvent(Media media) // Remove the controls RegionScene.Children.Remove(media); + + // Does this media stop finish stopping the region? + if (IsStopped) + { + OnRegionStopped?.Invoke(); + } } /// @@ -770,11 +790,6 @@ private void Media_DurationElapsedEvent(int filesPlayed) return; } - // TODO: - // Animate out at this point if we need to - // the result of the animate out complete event should then move us on. - // this.currentMedia.TransitionOut(); - // make some decisions about what to do next try { @@ -828,8 +843,10 @@ private void SetDimensions(int left, int top, int width, int height) /// Clears the Region of anything that it shouldnt still have... /// called when Destroying a Layout and when Removing an Overlay /// - public void Clear() + public void Stop() { + IsStopped = true; + try { // Stop Audio @@ -840,10 +857,15 @@ public void Clear() { StopMedia(this.currentMedia); } + else + { + // We are stopped immediately + OnRegionStopped?.Invoke(); + } } catch { - Trace.WriteLine(new LogMessage("Region - Clear", "Error closing off stat record"), LogType.Error.ToString()); + Trace.WriteLine(new LogMessage("Region", "Stop: error stopping region"), LogType.Error.ToString()); } } From d884f833d342a823e76e20287df6178de8882e88 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 10 Mar 2022 14:34:09 +0000 Subject: [PATCH 5/6] Interactive: overlay actions aren't considered always check from overlays as well as the current layout #248 --- MainWindow.xaml.cs | 122 ++++++++++++++++++++++++++++++++++----- Rendering/Layout.xaml.cs | 18 ++++++ 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index e639e3e..8660156 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -967,6 +967,81 @@ public void ManageOverlays(List overlays) } } + /// + /// Get actions + /// + /// + private List GetActions() + { + List actions = new List(); + + // Pull actions from the main layout and any overlays + if (currentLayout != null) + { + actions.AddRange(currentLayout.GetActions()); + } + + // Add overlays + foreach (Layout overlay in _overlays) + { + actions.AddRange(overlay.GetActions()); + } + + return actions; + } + + /// + /// Is the provided widgetId playing? + /// + /// + /// + private bool IsWidgetIdPlaying(int sourceId) + { + if (currentLayout != null) + { + if (currentLayout.IsWidgetIdPlaying("" + sourceId)) + { + return true; + } + } + + foreach (Layout overlay in _overlays) + { + if (overlay.IsWidgetIdPlaying("" + sourceId)) + { + return true; + } + } + + return false; + } + + /// + /// Is the provided widgetId playing? + /// + /// + /// + private bool IsWidgetIdPlayingInRegion(Point point, int sourceId) + { + if (currentLayout != null) + { + if (currentLayout.GetCurrentWidgetIdForRegion(point).Contains("" + sourceId)) + { + return true; + } + } + + foreach (Layout overlay in _overlays) + { + if (overlay.GetCurrentWidgetIdForRegion(point).Contains("" + sourceId)) + { + return true; + } + } + + return false; + } + /// /// Handle Action trigger from a Trigger /// @@ -1005,16 +1080,9 @@ public void HandleActionTrigger(string triggerType, string triggerCode, int sour /// public void HandleActionTrigger(string triggerType, string triggerCode, int sourceId, Point point) { - // If we're interrupting we don't process any actions. - if (currentLayout == null) - { - Debug.WriteLine("HandleActionTrigger: Skipping Action as current Layout is not set.", "MainWindow"); - return; - } - // Do we have any actions which match this trigger type? // These are in order, with Widgets first. - foreach (Action.Action action in currentLayout.GetActions()) + foreach (Action.Action action in GetActions()) { // Match the trigger type if (action.TriggerType != triggerType) @@ -1034,7 +1102,7 @@ public void HandleActionTrigger(string triggerType, string triggerCode, int sour continue; } // Webhooks coming from a Widget must be active somewhere on the Layout - else if (triggerType == "webhook" && action.Source == "widget" && !currentLayout.IsWidgetIdPlaying("" + action.SourceId)) + else if (triggerType == "webhook" && action.Source == "widget" && !IsWidgetIdPlaying(action.SourceId)) { Debug.WriteLine(point.ToString() + " webhook matches widget which isn't playing: " + action.SourceId, "HandleActionTrigger"); continue; @@ -1046,7 +1114,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).Contains("" + action.SourceId)) + else if (triggerType == "touch" && action.Source == "widget" && !IsWidgetIdPlayingInRegion(point, action.SourceId)) { Debug.WriteLine(point.ToString() + " not active widget: " + action.SourceId, "HandleActionTrigger"); continue; @@ -1120,15 +1188,39 @@ public void ExecuteAction(Action.Action action) case "navWidget": // Navigate to the provided Widget - if (action.Target == "screen") + // A widget action could come from a normal Layout or an overlay, which is it? + if (currentLayout.HasWidgetIdInDrawer(action.WidgetId)) { - // Expect a shell command. - currentLayout.ExecuteWidget(action.WidgetId); + if (action.Target == "screen") + { + // Expect a shell command. + currentLayout.ExecuteWidget(action.WidgetId); + } + else + { + // Provided Widget in the named region + currentLayout.RegionChangeToWidget(action.TargetId + "", action.WidgetId); + } } else { - // Provided Widget in the named region - currentLayout.RegionChangeToWidget(action.TargetId + "", action.WidgetId); + // Check in overlays + foreach (Layout overlay in _overlays) + { + if (overlay.HasWidgetIdInDrawer(action.WidgetId)) + { + if (action.Target == "screen") + { + // Expect a shell command. + overlay.ExecuteWidget(action.WidgetId); + } + else + { + // Provided Widget in the named region + overlay.RegionChangeToWidget(action.TargetId + "", action.WidgetId); + } + } + } } break; diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 8578bd7..262ac79 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -852,6 +852,24 @@ private XmlNode GetWidgetFromDrawer(int widgetId) throw new Exception("Drawer does not contain a Widget with widgetId " + widgetId); } + /// + /// Does this layout have a widget in its drawer matching the provided ID + /// + /// + /// + public bool HasWidgetIdInDrawer(int widgetId) + { + try + { + GetWidgetFromDrawer(widgetId); + return true; + } + catch + { + return false; + } + } + /// /// Get Actions /// From b5e7500dc4c0f09d55eb8d7f3808f8ec9c468c5d Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 10 Mar 2022 17:45:32 +0000 Subject: [PATCH 6/6] Schedule Actions #234 Also fixed an issue returning from a navigate layout to a layout which already had a navigate widget running --- Action/Action.cs | 31 ++++++++++++++- Helpers/GeoHelper.cs | 71 +++++++++++++++++++++++++++++++++ Logic/Schedule.cs | 11 +++++- Logic/ScheduleItem.cs | 31 +++++---------- Logic/ScheduleManager.cs | 84 +++++++++++++++++++++++++++++++++++++++- MainWindow.xaml.cs | 31 ++++++++++++++- Rendering/Layout.xaml.cs | 1 + Rendering/Region.xaml.cs | 27 ++++++++++++- XiboClient.csproj | 1 + 9 files changed, 261 insertions(+), 27 deletions(-) create mode 100644 Helpers/GeoHelper.cs diff --git a/Action/Action.cs b/Action/Action.cs index caf7f68..5666e2c 100644 --- a/Action/Action.cs +++ b/Action/Action.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -43,6 +43,7 @@ public class Action public string LayoutCode { get; set; } public bool Bubble { get; set; } public bool IsDrawer { get; set; } + public string CommandCode { get; set; } public Rect Rect { get; set; } @@ -73,6 +74,34 @@ public static Action CreateFromXmlNode(XmlNode node, int top, int left, int widt }; } + /// + /// Create an action from a schedule XML node + /// + /// + /// + public static Action CreateFromScheduleNode(XmlNode node) + { + XmlAttributeCollection attributes = node.Attributes; + + return new Action + { + Id = int.Parse(attributes["scheduleid"].Value), + ActionType = attributes["actionType"]?.Value, + TriggerType = "webhook", + TriggerCode = attributes["triggerCode"]?.Value, + WidgetId = 0, + SourceId = 0, + Source = "schedule", + TargetId = 0, + Target = "screen", + LayoutCode = attributes["layoutCode"]?.Value, + Bubble = false, + IsDrawer = false, + CommandCode = attributes["commandCode"]?.Value, + Rect = new Rect(0, 0, 0, 0) + }; + } + /// /// Create a list of Actions from an XmlNodeList /// diff --git a/Helpers/GeoHelper.cs b/Helpers/GeoHelper.cs new file mode 100644 index 0000000..b2d0093 --- /dev/null +++ b/Helpers/GeoHelper.cs @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2022 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 GeoJSON.Net.Contrib.MsSqlSpatial; +using GeoJSON.Net.Feature; +using GeoJSON.Net.Geometry; +using Microsoft.SqlServer.Types; +using Newtonsoft.Json; +using System; +using System.Device.Location; +using System.Diagnostics; + +namespace XiboClient.Helpers +{ + class GeoHelper + { + /// + /// Is the provided geoJson inside the provided point + /// + /// + /// + /// + public static bool IsGeoInPoint(string geoJson, Point point) + { + try + { + // Test against the geo location + var geo = JsonConvert.DeserializeObject(geoJson); + + // Use SQL spatial helper to calculate intersection or not + SqlGeometry polygon = (geo.Geometry as Polygon).ToSqlGeometry(); + + return point.ToSqlGeometry().STIntersects(polygon).Value; + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("GeoHelper", "IsGeoInPoint: Cannot parse geo location: e = " + e.Message), LogType.Audit.ToString()); + + return false; + } + } + + /// + /// Is the provided geoJson inside the provided point, denoted by a location + /// + /// + /// + /// + public static bool IsGeoInPoint(string geoJson, GeoCoordinate geoCoordinate) + { + return IsGeoInPoint(geoJson, new Point(new Position(geoCoordinate.Latitude, geoCoordinate.Longitude))); + } + } +} diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index a303f6b..0b303c4 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -783,5 +783,14 @@ public Ad GetAd(double width, double height) { return _scheduleManager.GetAd(width, height); } + + /// + /// Get the current actions schedule + /// + /// + public List GetActions() + { + return _scheduleManager.CurrentActionsSchedule; + } } } diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index 1b29306..62a5e20 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Device.Location; using System.Diagnostics; +using XiboClient.Helpers; namespace XiboClient.Logic { @@ -194,29 +195,17 @@ public bool SetIsGeoActive(GeoCoordinate geoCoordinate) } else { - try - { - // Current location. - Point current = new Point(new Position(geoCoordinate.Latitude, geoCoordinate.Longitude)); - - // Have we already tested this? - if (this.testedAgainst == null || !testedAgainst.Equals(current)) - { - // Not tested yet, or position changed. - this.testedAgainst = current; - - // Test against the geo location - var geo = JsonConvert.DeserializeObject(GeoLocation); + // Current location. + Point current = new Point(new Position(geoCoordinate.Latitude, geoCoordinate.Longitude)); - // Use SQL spatial helper to calculate intersection or not - SqlGeometry polygon = (geo.Geometry as Polygon).ToSqlGeometry(); - - IsGeoActive = current.ToSqlGeometry().STIntersects(polygon).Value; - } - } - catch (Exception e) + // Have we already tested this? + if (this.testedAgainst == null || !testedAgainst.Equals(current)) { - Trace.WriteLine(new LogMessage("ScheduleItem", "SetIsGeoActive: Cannot parse geo location: e = " + e.Message), LogType.Audit.ToString()); + // Not tested yet, or position changed. + this.testedAgainst = current; + + // Test + IsGeoActive = GeoHelper.IsGeoInPoint(GeoLocation, current); } } diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index b1334ae..61b893e 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.cs @@ -1,5 +1,5 @@ /** - * Copyright (C) 2021 Xibo Signage Ltd + * Copyright (C) 2022 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -29,6 +29,7 @@ using System.Xml; using XiboClient.Action; using XiboClient.Adspace; +using XiboClient.Helpers; using XiboClient.Log; using XiboClient.Logic; @@ -65,6 +66,7 @@ class ScheduleManager private List _commands; private List _overlaySchedule; private List _invalidSchedule; + private List _actionsSchedule; // State private bool _refreshSchedule; @@ -93,6 +95,10 @@ public ScheduleManager(string scheduleLocation) _overlaySchedule = new List(); _overlayLayoutActions = new List(); + // Action schedules + CurrentActionsSchedule = new List(); + _actionsSchedule = new List(); + // Screenshot _lastScreenShotDate = DateTime.MinValue; @@ -135,6 +141,11 @@ public bool RefreshSchedule /// public List CurrentOverlaySchedule { get; private set; } + /// + /// The current scheduled actions + /// + public List CurrentActionsSchedule { get; private set; } + #endregion /// @@ -395,6 +406,9 @@ private bool IsNewScheduleAvailable() // Clear the list of overlays _overlaySchedule.Clear(); + // Clear the list of actions + _actionsSchedule.Clear(); + // Load in the schedule LoadScheduleFromFile(); @@ -503,6 +517,9 @@ private bool IsNewScheduleAvailable() // Set the new Overlay schedule CurrentOverlaySchedule = overlaySchedule; + // Set the Actions schedule + CurrentActionsSchedule = _actionsSchedule; + // Return True if we want to refresh the schedule OR false if we are OK to leave the current one. // We can update the current schedule and still return false - this will not trigger a schedule change event. // We do this if ALL the current layouts are still in the schedule @@ -1034,6 +1051,10 @@ private void LoadScheduleFromFile() _overlaySchedule.Add(ParseNodeIntoScheduleItem(overlayNode)); } } + else if (node.Name == "actions") + { + ParseNodeListIntoActions(node.ChildNodes); + } else { _layoutSchedule.Add(ParseNodeIntoScheduleItem(node)); @@ -1185,6 +1206,67 @@ private ScheduleItem ParseNodeIntoScheduleItem(XmlNode node) return temp; } + /// + /// Parse a node list of actions into actual actions + /// + /// + private void ParseNodeListIntoActions(XmlNodeList nodes) + { + // Track the highest priority + int highestPriority = 0; + + foreach (XmlNode node in nodes) + { + XmlAttributeCollection attributes = node.Attributes; + + // Priority flag + int actionPriority; + try + { + actionPriority = int.Parse(attributes["priority"].Value); + } + catch + { + actionPriority = 0; + } + + // Get the fromdt,todt + DateTime fromDt = DateTime.Parse(attributes["fromdt"].Value, CultureInfo.InvariantCulture); + DateTime toDt = DateTime.Parse(attributes["todt"].Value, CultureInfo.InvariantCulture); + + if (DateTime.Now > fromDt && DateTime.Now < toDt) + { + // Geo Schedule + if (attributes["isGeoAware"] != null && attributes["isGeoAware"].Value == "1") + { + // Test the geo location and skip if we're outside + string geoLocation = attributes["geoLocation"] != null ? attributes["geoLocation"].Value : ""; + if (string.IsNullOrEmpty(geoLocation) + || ClientInfo.Instance.CurrentGeoLocation == null + || ClientInfo.Instance.CurrentGeoLocation.IsUnknown) + { + continue; + } + + // Test the geolocation + if (!GeoHelper.IsGeoInPoint(geoLocation, ClientInfo.Instance.CurrentGeoLocation)) + { + continue; + } + } + + // is this a new high watermark for priority + if (actionPriority > highestPriority) + { + _actionsSchedule.Clear(); + highestPriority = actionPriority; + } + + _actionsSchedule.Add(Action.Action.CreateFromScheduleNode(node)); + } + } + } + /// /// Load schedule from layout change actions /// diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 8660156..685c566 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.Action; using XiboClient.Adspace; using XiboClient.Error; using XiboClient.Log; @@ -499,7 +500,7 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) this.currentLayout.Stop(); - Debug.WriteLine("ChangeToNextLayout: stopped and removed the current Layout", "MainWindow"); + Debug.WriteLine("ChangeToNextLayout: stopped and removed the current Layout: " + this.currentLayout.UniqueId, "MainWindow"); } } catch (Exception e) @@ -987,6 +988,9 @@ public void ManageOverlays(List overlays) actions.AddRange(overlay.GetActions()); } + // Add the current schedule actions + actions.AddRange(_schedule.GetActions()); + return actions; } @@ -1182,8 +1186,9 @@ public void ExecuteAction(Action.Action action) case "navLayout": // Navigate to the provided Layout // target is always screen - ChangeToNextLayout(_schedule.GetScheduleItemForLayoutCode(action.LayoutCode)); + Debug.WriteLine("MainWindow", "ExecuteAction: change to next layout with code " + action.LayoutCode); + ChangeToNextLayout(_schedule.GetScheduleItemForLayoutCode(action.LayoutCode)); break; case "navWidget": @@ -1225,6 +1230,28 @@ public void ExecuteAction(Action.Action action) break; + case "command": + // Run a command directly + if (action.Target == "screen") + { + // Expect a stored command. + try + { + Command command = Command.GetByCode(action.CommandCode); + command.Run(); + } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("MainWindow", "ExecuteAction: cannot run Command: " + e.Message), LogType.Error.ToString()); + } + } + else + { + // Not supported + Trace.WriteLine(new LogMessage("MainWindow", "ExecuteAction: command actions must be targeted to the screen."), LogType.Audit.ToString()); + } + break; + default: Trace.WriteLine(new LogMessage("MainWindow", "ExecuteAction: unknown type: " + action.ActionType), LogType.Error.ToString()); break; diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 262ac79..72c96c8 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -652,6 +652,7 @@ public void Remove() try { // Clear the region + region.Clear(); region.DurationElapsedEvent -= Region_DurationElapsedEvent; region.MediaExpiredEvent -= Region_MediaExpiredEvent; region.OnRegionStopped -= Region_OnRegionStopped; diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index b2d56fe..0af26bf 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -239,6 +239,8 @@ public void Next() /// A Media XmlNode public void NavigateToWidget(XmlNode node) { + Debug.WriteLine("Region", "Navigating to widget in Region: " + Id); + // Create the options and media node Media media = CreateNextMediaNode(Media.ParseOptions(node)); @@ -268,6 +270,9 @@ public void NavigateToWidget(XmlNode node) // Switch-a-roo navigatedMedia = media; + + // Current media is still set. + Debug.WriteLine("Region", "Navigated to widget in Region: " + Id); } catch (Exception e) { @@ -853,7 +858,11 @@ public void Stop() StopAudio(); // Stop the current media item - if (this.currentMedia != null) + if (this.navigatedMedia != null) + { + StopMedia(this.navigatedMedia); + } + else if (this.currentMedia != null) { StopMedia(this.currentMedia); } @@ -869,6 +878,22 @@ public void Stop() } } + /// + /// Clear out any remaining stuff. + /// + public void Clear() + { + if (this.navigatedMedia != null) + { + this.navigatedMedia = null; + } + + if (this.currentMedia != null) + { + this.currentMedia = null; + } + } + /// /// Set any adspace exchange impression urls. /// diff --git a/XiboClient.csproj b/XiboClient.csproj index bda91f9..b4545b9 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -129,6 +129,7 @@ +