diff --git a/InfoScreen.xaml.cs b/InfoScreen.xaml.cs index a7db3bef..e138fe71 100644 --- a/InfoScreen.xaml.cs +++ b/InfoScreen.xaml.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 * diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index a9694c7b..7c071abd 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.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 * @@ -50,9 +50,9 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "2 R256.7"; + public string ClientVersion { get; } = "2 R257.1"; public string Version { get; } = "5"; - public int ClientCodeVersion { get; } = 256; + public int ClientCodeVersion { get; } = 257; private ApplicationSettings() { @@ -579,6 +579,7 @@ public bool InDownloadWindow public bool PreventSleep { get; set; } public bool ScreenShotRequested { get; set; } public bool FallbackToInternetExplorer { get; set; } + public bool IsRecordGeoLocationOnProofOfPlay { get; set; } // XMDS Status Flags private DateTime _xmdsLastConnection; diff --git a/Logic/CacheManager.cs b/Logic/CacheManager.cs index f5e8f9b6..ec611fc0 100644 --- a/Logic/CacheManager.cs +++ b/Logic/CacheManager.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 * @@ -19,6 +19,7 @@ * along with Xibo. If not, see . */ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; @@ -51,6 +52,10 @@ public static CacheManager Instance /// private Collection _unsafeItems = new Collection(); + /// + /// List of layout durations + /// + private Dictionary _layoutDurations = new Dictionary(); private CacheManager() { @@ -480,6 +485,38 @@ private string UnsafeListAsString() } #endregion + + #region Layout Durations + + /// + /// Record Layout Duration + /// + /// + /// + public void RecordLayoutDuration(int layoutId, int duration) + { + _layoutDurations[layoutId] = duration; + } + + /// + /// Get Layout Duration + /// + /// + /// + /// + public int GetLayoutDuration(int layoutId, int defaultValue) + { + if (_layoutDurations.ContainsKey(layoutId)) + { + return _layoutDurations[layoutId]; + } + else + { + return defaultValue; + } + } + + #endregion } /// diff --git a/Logic/InterruptState.cs b/Logic/InterruptState.cs deleted file mode 100644 index dcd3a5bf..00000000 --- a/Logic/InterruptState.cs +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (C) 2020 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 System; -using System.Collections.Generic; - -namespace XiboClient.Logic -{ - class InterruptState - { - public double SecondsInterrutedThisPeriod = 0; - public int TargetHourlyInterruption = 0; - public DateTime LastInterruption; - public DateTime LastPlaytimeUpdate; - public DateTime LastInterruptScheduleChange; - public Dictionary InterruptTracking; - - /// - /// Get an empty Interrupt State - /// - /// - public static InterruptState EmptyState() - { - // set the dates to just enough in the past for them to get reset. - return new InterruptState() - { - LastInterruption = DateTime.Now.AddHours(-2), - LastPlaytimeUpdate = DateTime.Now.AddHours(-2), - LastInterruptScheduleChange = DateTime.Now.AddHours(-2), - InterruptTracking = new Dictionary() - }; - } - } -} diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 919835b2..bc6b5109 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.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 * @@ -36,7 +36,7 @@ namespace XiboClient /// public class Schedule { - public delegate void ScheduleChangeDelegate(ScheduleItem scheduleItem, string mode); + public delegate void ScheduleChangeDelegate(ScheduleItem scheduleItem); public event ScheduleChangeDelegate ScheduleChangeEvent; public delegate void OverlayChangeDelegate(List overlays); @@ -47,7 +47,6 @@ public class Schedule /// private List _layoutSchedule; private int _currentLayout = 0; - private int _currentInterruptLayout = 0; /// /// Current Schedule of Overlay Layouts @@ -59,11 +58,6 @@ public class Schedule /// private bool _stopCalled = false; - /// - /// Are we currently interrupting? - /// - private bool _interrupting = false; - #region Threads and Agents // Key private HardwareKey _hardwareKey; @@ -122,9 +116,6 @@ public Schedule(string scheduleLocation) _scheduleManager.OnNewScheduleAvailable += new ScheduleManager.OnNewScheduleAvailableDelegate(_scheduleManager_OnNewScheduleAvailable); _scheduleManager.OnRefreshSchedule += new ScheduleManager.OnRefreshScheduleDelegate(_scheduleManager_OnRefreshSchedule); _scheduleManager.OnScheduleManagerCheckComplete += _scheduleManager_OnScheduleManagerCheckComplete; - _scheduleManager.OnInterruptNow += _scheduleManager_OnInterruptNow; - _scheduleManager.OnInterruptPausePending += _scheduleManager_OnInterruptPausePending; - _scheduleManager.OnInterruptEnd += _scheduleManager_OnInterruptEnd; // Create a schedule manager thread _scheduleManagerThread = new Thread(new ThreadStart(_scheduleManager.Run)); @@ -216,19 +207,11 @@ private void _scheduleManager_OnNewScheduleAvailable() // Set the current pointer to 0 _currentLayout = 0; - // If we are not interrupting, then update the current schedule - if (!this._interrupting) - { - // Raise a schedule change event - ScheduleChangeEvent(_layoutSchedule[0], "next"); + // Raise a schedule change event + ScheduleChangeEvent(_layoutSchedule[0]); - // Pass a new set of overlay's to subscribers - OverlayChangeEvent?.Invoke(_overlaySchedule); - } - else - { - Debug.WriteLine("_scheduleManager_OnNewScheduleAvailable: Skipping Next Layout Change due to Interrupt", "Schedule"); - } + // Pass a new set of overlay's to subscribers + OverlayChangeEvent?.Invoke(_overlaySchedule); } /// @@ -401,84 +384,23 @@ public void restartXmr() /// public void NextLayout() { - Debug.WriteLine("NextLayout: called. Interrupting: " + this._interrupting, "Schedule"); + Debug.WriteLine("NextLayout: called", "Schedule"); - // Get the previous layout - ScheduleItem previousLayout = (this._interrupting) - ? _scheduleManager.CurrentInterruptSchedule[_currentInterruptLayout] - : _layoutSchedule[_currentLayout]; + // increment the current layout + _currentLayout++; - // See if the current layout is an action that can be removed. - // If it CAN be removed then this will almost certainly result in a change in the current _layoutSchedule - // therefore we should return out of this and kick off a schedule manager cycle, which will set the new layout. - try + // if the current layout is greater than the count of layouts, then reset to 0 + if (_currentLayout >= _layoutSchedule.Count) { - if (_scheduleManager.removeLayoutChangeActionIfComplete(previousLayout)) - { - _scheduleManager.RunNow(); - return; - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("Schedule", "NextLayout: Unable to check layout change actions. E = " + e.Message), LogType.Error.ToString()); + _currentLayout = 0; } - // Are currently interrupting? - ScheduleItem nextLayout; - if (this._interrupting) - { - // We might have fulifilled items in the schedule. - List notFulfilled = new List(); - foreach (ScheduleItem item in _scheduleManager.CurrentInterruptSchedule) - { - if (!item.IsFulfilled) - { - notFulfilled.Add(item); - } - } - - // What if we don't have any? - // pick the least worst option - if (notFulfilled.Count <= 0) - { - Debug.WriteLine("NextLayout: Interrupting and have run out of not-fulfilled schedules, using the first one.", "Schedule"); - - nextLayout = _scheduleManager.CurrentInterruptSchedule[0]; - } - else - { - // increment the current layout - _currentInterruptLayout++; - - // if the current layout is greater than the count of layouts, then reset to 0 - if (_currentInterruptLayout >= notFulfilled.Count) - { - _currentInterruptLayout = 0; - } - - // Pull out the next Layout - nextLayout = notFulfilled[_currentInterruptLayout]; - } - } - else - { - // increment the current layout - _currentLayout++; - - // if the current layout is greater than the count of layouts, then reset to 0 - if (_currentLayout >= _layoutSchedule.Count) - { - _currentLayout = 0; - } - - nextLayout = _layoutSchedule[_currentLayout]; - } + ScheduleItem nextLayout = _layoutSchedule[_currentLayout]; Debug.WriteLine(string.Format("NextLayout: {0}, Interrupt: {1}", nextLayout.layoutFile, nextLayout.IsInterrupt()), "Schedule"); // Raise the event - ScheduleChangeEvent?.Invoke(nextLayout, (this._interrupting ? "interrupt-next" : "next")); + ScheduleChangeEvent?.Invoke(nextLayout); } /// @@ -501,17 +423,6 @@ public int ActiveLayouts } } - /// - /// The number of active layouts in the current schedule - /// - public int ActiveInterruptLayouts - { - get - { - return _scheduleManager.CurrentInterruptSchedule.Count; - } - } - /// /// A layout file has changed /// @@ -522,8 +433,7 @@ private void LayoutFileModified(string layoutPath) // Are we set to expire modified layouts? If not then just return as if // nothing had happened. - // We never force change an interrupt layout - if (!ApplicationSettings.Default.ExpireModifiedLayouts || this._interrupting) + if (!ApplicationSettings.Default.ExpireModifiedLayouts) { return; } @@ -618,9 +528,6 @@ public void Stop() // Stop the Schedule Manager Thread _scheduleManager.Stop(); - _scheduleManager.OnInterruptNow -= _scheduleManager_OnInterruptNow; - _scheduleManager.OnInterruptPausePending -= _scheduleManager_OnInterruptPausePending; - _scheduleManager.OnInterruptEnd -= _scheduleManager_OnInterruptEnd; // Stop the LibraryAgent Thread _libraryAgent.Stop(); @@ -655,101 +562,30 @@ public void RemoveLayout(ScheduleItem item) } } - #region Interrupt Layouts - /// - /// Indicate we are interrupting + /// A change layout action has finished /// - public void SetInterrupting() - { - this._interrupting = true; - - // Inform the schedule manager that we have interrupted. - this._scheduleManager.InterruptSetActive(); - } - - /// - /// Interrupt Media has been Played - /// - public void SetInterruptMediaPlayed() - { - // Call interrupt end to switch back to the normal schedule - this._scheduleManager_OnInterruptEnd(); - } - - /// - /// Indicate there is an error with the Interrupt - /// - public void SetInterruptUnableToPlayAndEnd() - { - this._scheduleManager_OnInterruptEnd(); - } - - /// - /// Interrupt Ended - /// - private void _scheduleManager_OnInterruptEnd() - { - Debug.WriteLine("Interrupt End Event", "Schedule"); - - if (this._interrupting) - { - // Assume we will stop - this._interrupting = false; - - // Stop interrupting forthwith - ScheduleChangeEvent?.Invoke(null, "interrupt-end"); - - // Bring back overlays - OverlayChangeEvent?.Invoke(_overlaySchedule); - } - } - - /// - /// Interrupt should pause after playback - /// - private void _scheduleManager_OnInterruptPausePending() - { - Debug.WriteLine("Interrupt Pause Pending Event", "Schedule"); - - if (this._interrupting) - { - // Set Pause Pending on the current Interrupt Layout - ScheduleChangeEvent?.Invoke(null, "pause-pending"); - } - } - - /// - /// Interrupt should happen now - /// - private void _scheduleManager_OnInterruptNow() + /// + /// + public bool NotifyLayoutActionFinished(ScheduleItem item) { - Debug.WriteLine("Interrupt Now Event", "Schedule"); - - if (!this._interrupting && this._scheduleManager.CurrentInterruptSchedule.Count > 0) + // See if the current layout is an action that can be removed. + // If it CAN be removed then this will almost certainly result in a change in the current _layoutSchedule + // therefore we should return out of this and kick off a schedule manager cycle, which will set the new layout. + try { - // Remove overlays - if (_overlaySchedule != null && _overlaySchedule.Count > 0) + if (_scheduleManager.removeLayoutChangeActionIfComplete(item)) { - OverlayChangeEvent?.Invoke(new List()); + _scheduleManager.RunNow(); + return true; } - - // Choose the interrupt in position 0 - ScheduleChangeEvent?.Invoke(this._scheduleManager.CurrentInterruptSchedule[0], "interrupt"); } - } + catch (Exception e) + { + Trace.WriteLine(new LogMessage("Schedule", "NotifyLayoutActionFinished: Unable to check layout change actions. E = " + e.Message), LogType.Error.ToString()); + } - /// - /// Report the Play durarion of the current layout. - /// - /// - /// - /// - public void CurrentLayout_OnReportLayoutPlayDurationEvent(int scheduleId, int layoutId, double duration) - { - this._scheduleManager.InterruptRecordSecondsPlayed(scheduleId, duration); + return false; } - - #endregion } } diff --git a/Logic/ScheduleItem.cs b/Logic/ScheduleItem.cs index 76bc4f36..c4545abe 100644 --- a/Logic/ScheduleItem.cs +++ b/Logic/ScheduleItem.cs @@ -40,9 +40,9 @@ public class ScheduleItem public int ShareOfVoice; /// - /// Seconds Played + /// The duration of this event /// - public double SecondsPlayed; + public int Duration; // Geo Schedule public bool IsGeoAware = false; @@ -60,14 +60,14 @@ public class ScheduleItem public bool Refresh = false; /// - /// Is this schedule item fulfilled - used for Interrupts + /// Point we have tested against for GeoSchedule /// - public bool IsFulfilled = false; + private Point testedAgainst; /// - /// Point we have tested against for GeoSchedule + /// Duration committed /// - private Point testedAgainst; + private int durationCommitted = 0; /// /// ToString @@ -75,7 +75,10 @@ public class ScheduleItem /// public override string ToString() { - return string.Format("[{0}] From {1} to {2} with priority {3}. {4} dependents.", id, FromDt.ToString(), ToDt.ToString(), Priority, Dependents.Count); + return "[" + id + "] " + + (IsInterrupt() ? "(I) " : " ") + + "P" + Priority + ; } /// @@ -185,36 +188,29 @@ public bool SetIsGeoActive(GeoCoordinate geoCoordinate) } /// - /// Calculate a Rank for this Item + /// Add to the committed duration /// - /// - /// - public double CalculateRank(int secondsToPeriodEnd) + /// + public void AddCommittedDuration(int duration) { - if (ShareOfVoice <= 0 || SecondsPlayed >= ShareOfVoice) - { - return 0; - } - else - { - double completeDifficulty = (ShareOfVoice - SecondsPlayed) / Convert.ToDouble(ShareOfVoice); - double scheduleDifficulty = (secondsToPeriodEnd - RemainingScheduledTime()) / secondsToPeriodEnd; - - return completeDifficulty + scheduleDifficulty; - } + this.durationCommitted += duration; } /// - /// Get remaining scheduled time in seconds + /// Is the duration requested satisfied? /// /// - public double RemainingScheduledTime() + public bool IsDurationSatisfied() { - DateTime now = DateTime.Now; - DateTime endOfHour = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0).AddHours(1); - DateTime endOfScheduleOrHour = (endOfHour > ToDt) ? ToDt : endOfHour; + return this.durationCommitted >= this.ShareOfVoice; + } - return (endOfScheduleOrHour - now).TotalSeconds; + /// + /// Reset the committed duration for another pass. + /// + public void ResetCommittedDuration() + { + this.durationCommitted = 0; } } } diff --git a/Logic/ScheduleItemComparer.cs b/Logic/ScheduleItemComparer.cs deleted file mode 100644 index 68b689a5..00000000 --- a/Logic/ScheduleItemComparer.cs +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (C) 2020 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 System.Collections.Generic; -using System.Diagnostics; - -namespace XiboClient.Logic -{ - class ScheduleItemComparer : IComparer - { - private int secondsToPeriodEnd; - - public ScheduleItemComparer(int secondsToPeriodEnd) - { - this.secondsToPeriodEnd = secondsToPeriodEnd; - } - - public int Compare(ScheduleItem x, ScheduleItem y) - { - // Calculate ranks - double rankX = x.CalculateRank(this.secondsToPeriodEnd); - double rankY = y.CalculateRank(this.secondsToPeriodEnd); - - Debug.WriteLine("Compare: scheduleId " + x.scheduleid + " with rank " + rankX - + " / scheduleId " + y.scheduleid + " with rank " + rankY, "ScheduleItemComparer"); - - // Calculate the rank for each item - return rankX > rankY ? 1 : -1; - } - } -} diff --git a/Logic/ScheduleManager.cs b/Logic/ScheduleManager.cs index 7c1b5e46..ed646589 100644 --- a/Logic/ScheduleManager.cs +++ b/Logic/ScheduleManager.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 * @@ -18,7 +18,6 @@ * 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.Device.Location; @@ -65,16 +64,6 @@ class ScheduleManager private List _commands; private List _overlaySchedule; private List _invalidSchedule; - private InterruptState _interruptState; - - public delegate void OnInterruptNowDelegate(); - public event OnInterruptNowDelegate OnInterruptNow; - - public delegate void OnInterruptPausePendingDelegate(); - public event OnInterruptPausePendingDelegate OnInterruptPausePending; - - public delegate void OnInterruptEndDelegate(); - public event OnInterruptEndDelegate OnInterruptEnd; // State private bool _refreshSchedule; @@ -100,9 +89,6 @@ public ScheduleManager(string scheduleLocation) _overlaySchedule = new List(); _overlayLayoutActions = new List(); - // Interrupts - CurrentInterruptSchedule = new List(); - // Screenshot _lastScreenShotDate = DateTime.MinValue; } @@ -142,12 +128,6 @@ public bool RefreshSchedule /// public List CurrentOverlaySchedule { get; private set; } - - /// - /// Get the current interrupt schedule - /// - public List CurrentInterruptSchedule { get; private set; } - #endregion /// @@ -191,9 +171,6 @@ public void Run() Trace.WriteLine(new LogMessage("ScheduleManager", "Run: GeoCoordinateWatcher failed to start. E = " + e.Message), LogType.Error.ToString()); } - // Load the interrupt state - InterruptInitState(); - // Run loop // -------- while (!_forceStop) @@ -208,25 +185,9 @@ public void Run() Trace.WriteLine(new LogMessage("ScheduleManager - Run", "Schedule Timer Ticked"), LogType.Audit.ToString()); // Work out if there is a new schedule available, if so - raise the event - bool isNewScheduleAvailable = IsNewScheduleAvailable(); - - // Interrupts - // ---------- - // Handle interrupts to keep the list in order and fresh - // this effectively sets the order of our interrupt layouts before they get updated on the main - // thread. - try - { - InterruptAssessAndUpdate(); - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("ScheduleManager", "Run: Problem assessing interrupt schedule. E = " + e.Message), LogType.Error.ToString()); - } - // Events // ------ - if (isNewScheduleAvailable) + if (IsNewScheduleAvailable()) { OnNewScheduleAvailable(); } @@ -298,9 +259,6 @@ public void Run() watcher.Dispose(); } - // Save the interrupt state - InterruptPersistState(); - Trace.WriteLine(new LogMessage("ScheduleManager - Run", "Thread Stopped"), LogType.Info.ToString()); } @@ -453,13 +411,18 @@ private bool IsNewScheduleAvailable() } // Load the new Schedule - List newSchedule = LoadNewSchedule(); + List parsedSchedule = ParseScheduleAndValidate(); // Load a new overlay schedule List overlaySchedule = LoadNewOverlaySchedule(); - // Load a new interrupt schedule - List newInterruptSchedule = LoadNewSchedule(true); + // Do we have any change layout actions? + List newSchedule = GetOverrideSchedule(parsedSchedule); + if (newSchedule.Count <= 0) + { + // No overrides, so we parse in our normal/interrupt layout mix. + newSchedule = ResolveNormalAndInterrupts(parsedSchedule); + } // Should we force a change // (broadly this depends on whether or not the schedule has changed.) @@ -471,12 +434,6 @@ private bool IsNewScheduleAvailable() forceChange = true; } - // Log - List currentScheduleString = new List(); - List newScheduleString = new List(); - List newOverlaysString = new List(); - List newInterruptString = new List(); - // Are all the items that were in the _currentSchedule still there? foreach (ScheduleItem layout in CurrentSchedule) { @@ -485,27 +442,8 @@ private bool IsNewScheduleAvailable() Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "New Schedule does not contain " + layout.id), LogType.Audit.ToString()); forceChange = true; } - currentScheduleString.Add(layout.ToString()); } - foreach (ScheduleItem layout in newSchedule) - { - newScheduleString.Add(layout.ToString()); - } - - Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "Layouts in Current Schedule: " + string.Join(Environment.NewLine, currentScheduleString)), LogType.Audit.ToString()); - Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "Layouts in New Schedule: " + string.Join(Environment.NewLine, newScheduleString)), LogType.Audit.ToString()); - - // Overlays - // -------- - // Logging first - foreach (ScheduleItem layout in overlaySchedule) - { - newOverlaysString.Add(layout.ToString()); - } - - Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "Overlay Layouts: " + string.Join(Environment.NewLine, newOverlaysString)), LogType.Audit.ToString()); - // Try to work out whether the overlay schedule has changed or not. // easiest way to do this is to see if the sizes have changed if (CurrentOverlaySchedule.Count != overlaySchedule.Count) @@ -520,31 +458,12 @@ private bool IsNewScheduleAvailable() { // New overlay schedule doesn't contain the layout? if (!overlaySchedule.Contains(layout)) + { forceChange = true; + } } } - // Interrupts - // ---------- - // We don't want a change in interrupt schedule to forceChange, because we don't want to impact the usual running schedule. - // But we do want to know if its happened - foreach (ScheduleItem layout in CurrentInterruptSchedule) - { - if (!newInterruptSchedule.Contains(layout)) - { - this._interruptState.LastInterruptScheduleChange = DateTime.Now; - Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "Interrupt Schedule Change"), LogType.Audit.ToString()); - } - } - - // Logging - foreach (ScheduleItem layout in newInterruptSchedule) - { - newInterruptString.Add(layout.ToString()); - } - - Trace.WriteLine(new LogMessage("ScheduleManager - IsNewScheduleAvailable", "Interrupt Layouts: " + string.Join(Environment.NewLine, newInterruptString)), LogType.Audit.ToString()); - // Finalise // -------- // Set the new schedule @@ -553,14 +472,6 @@ private bool IsNewScheduleAvailable() // Set the new Overlay schedule CurrentOverlaySchedule = overlaySchedule; - // Set the new interrupt schedule - this.CurrentInterruptSchedule = newInterruptSchedule; - - // Clear up - newSchedule = null; - overlaySchedule = null; - newInterruptSchedule = null; - // 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 @@ -571,22 +482,10 @@ private bool IsNewScheduleAvailable() /// Loads a new schedule from _layoutSchedules /// /// - private List LoadNewSchedule() - { - return LoadNewSchedule(false); - } - - /// - /// Loads a new schedule from _layoutSchedules - /// - /// Is this schedule for interrupt or normal - /// - private List LoadNewSchedule(bool isForInterrupt) + private List ParseScheduleAndValidate() { // We need to build the current schedule from the layout schedule (obeying date/time) - List newSchedule = new List(); - List prioritySchedule = new List(); - List layoutChangeSchedule = new List(); + List resolvedSchedule = new List(); // Temporary default Layout incase we have no layout nodes. ScheduleItem defaultLayout = new ScheduleItem(); @@ -595,23 +494,14 @@ private List LoadNewSchedule(bool isForInterrupt) List validLayoutIds = new List(); List invalidLayouts = new List(); - // Store the highest priority - int highestPriority = 0; - // For each layout in the schedule determine if it is currently inside the _currentSchedule, and whether it should be foreach (ScheduleItem layout in _layoutSchedule) { - // Pick only the ones we're interested in - if ((isForInterrupt && !layout.IsInterrupt()) - || (!isForInterrupt && layout.IsInterrupt())) - { - // Skip - continue; - } - // Is this already invalid if (invalidLayouts.Contains(layout.id)) + { continue; + } // If we haven't already assessed this layout before, then check that it is valid if (!validLayoutIds.Contains(layout.id)) @@ -686,54 +576,209 @@ private List LoadNewSchedule(bool isForInterrupt) } } - // Change Action and Priority layouts should generate their own list - if (layout.Override) - { - layoutChangeSchedule.Add(layout); - } - else if (layout.Priority >= 1) - { - // Is this higher than our priority already? - if (layout.Priority > highestPriority) - { - prioritySchedule.Clear(); - prioritySchedule.Add(layout); + resolvedSchedule.Add(layout); + } + } - // Store the new highest priority - highestPriority = layout.Priority; - } - else if (layout.Priority == highestPriority) + // Persist our new default. + CurrentDefaultLayout = defaultLayout; + + return resolvedSchedule; + } + + /// + /// Get Normal Schedule + /// + /// + /// + private List GetNormalSchedule(List schedule) + { + return GetHighestPriority(schedule.FindAll(i => i.IsInterrupt() == false)); + } + + /// + /// Get Interrupt Schedule + /// + /// + /// + private List GetInterruptSchedule(List schedule) + { + return GetHighestPriority(schedule.FindAll(i => i.IsInterrupt())); + } + + /// + /// Get Override Schedule + /// + /// + /// + private List GetOverrideSchedule(List schedule) + { + return schedule.FindAll(i => i.Override); + } + + /// + /// Get the highest priority schedule from a list of schedules. + /// + /// + /// + private List GetHighestPriority(List schedule) + { + int highestPriority = 0; + List resolved = new List(); + foreach (ScheduleItem item in schedule) + { + if (item.Priority > highestPriority) + { + resolved.Clear(); + highestPriority = item.Priority; + } + + if (item.Priority == highestPriority) + { + resolved.Add(item); + } + } + + return resolved; + } + + /// + /// Resolve normal and interrupts from a parsed valid schedule + /// + /// + /// + private List ResolveNormalAndInterrupts(List schedule) + { + // Clear any currently set durations + foreach (ScheduleItem item in schedule) + { + item.ResetCommittedDuration(); + } + + // Get the two schedules + List normal = GetNormalSchedule(schedule); + List interrupt = GetInterruptSchedule(schedule); + + if (interrupt.Count <= 0) + { + return normal; + } + + // If we have an empty normal schedule, pop the default in there + if (normal.Count <= 0) + { + normal = new List + { + CurrentDefaultLayout + }; + } + + // We do have interrupts + // organise the schedule loop so that our interrupts play according to their share of voice requirements. + List resolved = new List(); + List resolvedNormal = new List(); + List resolvedInterrupt = new List(); + + // Make a list of interrupt layouts which contain an instance of the event for each time that interrupt + // needs to play to fulfil its share of voice. + int index = 0; + int interruptSecondsInHour = 0; + + while (true) + { + if (index >= interrupt.Count) + { + // Start from the beginning + index = 0; + + bool allSatisfied = true; + foreach (ScheduleItem check in interrupt) + { + if (!check.IsDurationSatisfied()) { - prioritySchedule.Add(layout); + allSatisfied = false; + break; } - // Layouts with a priority lower than the current highest are discarded. } - else + + // We break out when all items are satisfied. + if (allSatisfied) { - newSchedule.Add(layout); + break; } } + + ScheduleItem item = interrupt[index]; + if (!item.IsDurationSatisfied()) + { + // The duration of this layout. + // from 2.3.10 CMS this is provided in XMDS + // if not provided, we use the last actual duration of the layout + int duration = (item.Duration <= 0) + ? CacheManager.Instance.GetLayoutDuration(item.id, 60) + : item.Duration; + + item.AddCommittedDuration(duration); + interruptSecondsInHour += duration; + resolvedInterrupt.Add(item); + } + + index++; } - // If we have any layout change scheduled then we return those instead - if (layoutChangeSchedule.Count > 0) - return layoutChangeSchedule; + // We will have some time remaining, so go through the normal layouts and produce a schedule + // to consume this remaining time + int normalSecondsInHour = 3600 - interruptSecondsInHour; + index = 0; - // If we have any priority schedules then we need to return those instead - if (prioritySchedule.Count > 0) - return prioritySchedule; + while (normalSecondsInHour > 0) + { + if (index >= normal.Count) + { + index = 0; + } - // If the current schedule is empty by the end of all this, then slip the default in - if (newSchedule.Count == 0 && !isForInterrupt) - newSchedule.Add(defaultLayout); + ScheduleItem item = normal[index]; + int duration = (item.Duration <= 0) + ? CacheManager.Instance.GetLayoutDuration(item.id, 60) + : item.Duration; - // Set the current default layout - if (!isForInterrupt) + normalSecondsInHour -= duration; + resolvedNormal.Add(item); + + index++; + } + + // Now we combine both schedules together, spreading the interrupts evenly + int pickCount = Math.Max(resolvedNormal.Count, resolvedInterrupt.Count); + int normalPick = (int)Math.Floor(1.0 * pickCount / resolvedNormal.Count); + int interruptPick = (int)Math.Floor(1.0 * pickCount / resolvedInterrupt.Count); + int normalIndex = 0; + int interruptIndex = 0; + + // Pick as many times as we need to consume the larger list + for (int i = 0; i < pickCount; i++) { - CurrentDefaultLayout = defaultLayout; + // We can overpick from the normal list + if (i % normalPick == 0) + { + if (normalIndex >= resolvedNormal.Count) + { + normalIndex = 0; + } + resolved.Add(resolvedNormal[normalIndex]); + normalIndex++; + } + + // We can't overpick from the interrupt list + if (i % interruptPick == 0 && interruptIndex < resolvedInterrupt.Count) + { + resolved.Add(resolvedInterrupt[interruptIndex]); + interruptIndex++; + } } - return newSchedule; + return resolved; } /// @@ -921,8 +966,10 @@ private void LoadScheduleFromFile() /// private ScheduleItem ParseNodeIntoScheduleItem(XmlNode node) { - ScheduleItem temp = new ScheduleItem(); - temp.NodeName = node.Name; + ScheduleItem temp = new ScheduleItem + { + NodeName = node.Name + }; // Pull attributes from layout nodes XmlAttributeCollection attributes = node.Attributes; @@ -1005,6 +1052,19 @@ private ScheduleItem ParseNodeIntoScheduleItem(XmlNode node) temp.ShareOfVoice = 0; } } + + // Duration + if (attributes["duration"] != null) + { + try + { + temp.Duration = int.Parse(attributes["duration"].Value); + } + catch + { + temp.Duration = 0; + } + } } // Look for dependents nodes @@ -1194,26 +1254,25 @@ private string LayoutsInSchedule() foreach (ScheduleItem layoutSchedule in CurrentSchedule) { if (layoutSchedule.Override) + { layoutsInSchedule += "API Action "; + } - layoutsInSchedule += "LayoutId: " + layoutSchedule.id + ". Runs from " + layoutSchedule.FromDt.ToString() + Environment.NewLine; + layoutsInSchedule += "Normal: " + layoutSchedule.ToString() + Environment.NewLine; } foreach (ScheduleItem layoutSchedule in CurrentOverlaySchedule) { - layoutsInSchedule += "Overlay LayoutId: " + layoutSchedule.id + ". Runs from " + layoutSchedule.FromDt.ToString() + Environment.NewLine; - } - - foreach (ScheduleItem layoutSchedule in CurrentInterruptSchedule) - { - layoutsInSchedule += "Interrupt LayoutId: " + layoutSchedule.id + ", shareOfVoice: " + layoutSchedule.ShareOfVoice + ". Runs from " + layoutSchedule.FromDt.ToString() + Environment.NewLine; + layoutsInSchedule += "Overlay: " + layoutSchedule.ToString() + Environment.NewLine; } foreach (ScheduleItem layoutSchedule in _invalidSchedule) { - layoutsInSchedule += "Invalid LayoutId: " + layoutSchedule.id + ". Should run from " + layoutSchedule.FromDt.ToString() + Environment.NewLine; + layoutsInSchedule += "Invalid: " + layoutSchedule.ToString() + Environment.NewLine; } + Trace.WriteLine(new LogMessage("ScheduleManager", "LayoutsInSchedule: " + layoutsInSchedule), LogType.Audit.ToString()); + return layoutsInSchedule; } @@ -1346,268 +1405,5 @@ public void setAllActionsDownloaded() } #endregion - - #region Interrupt Management - - /// - /// Update our schedule according to current standings - /// - private void InterruptAssessAndUpdate() - { - if (this.CurrentInterruptSchedule.Count <= 0) - { - // Fire an end event - OnInterruptEnd?.Invoke(); - } - else - { - // Recalculate the target hourly interruption - int targetHourlyInterruption = 0; - foreach (ScheduleItem item in CurrentInterruptSchedule) - { - targetHourlyInterruption += item.ShareOfVoice; - } - - // Did our schedule change last hour? - if (this._interruptState.LastInterruptScheduleChange < TopOfHour()) - { - // Just take the new figure - this._interruptState.TargetHourlyInterruption = targetHourlyInterruption; - } - else - { - this._interruptState.TargetHourlyInterruption = Math.Max(targetHourlyInterruption, this._interruptState.TargetHourlyInterruption); - } - - // Order the schedule and determine if we need to interrupt - InterruptResetSecondsIfNecessary(); - - // How far through the hour are we? - int secondsIntoHour = (int)(DateTime.Now - TopOfHour()).TotalSeconds; - int secondsIntoPeriod = (int)(DateTime.Now - TopOfPeriod()).TotalSeconds; - - // Assess each Layout and update the item with current understanding of seconds played and rank - bool hasNotFulfilledSchedule = false; - foreach (ScheduleItem item in CurrentInterruptSchedule) - { - try - { - if (this._interruptState.InterruptTracking.ContainsKey(item.scheduleid)) - { - // Annotate this item with the existing seconds played - this._interruptState.InterruptTracking.TryGetValue(item.scheduleid, out double secondsPlayed); - - item.SecondsPlayed = secondsPlayed; - - // Is this item fulfilled - item.IsFulfilled = (item.SecondsPlayed >= item.ShareOfVoice); - } - else - { - item.IsFulfilled = false; - item.SecondsPlayed = 0; - } - - // Set our watermark for whether we have a not-fulfilled schedule - if (!item.IsFulfilled) - { - hasNotFulfilledSchedule = true; - } - - Debug.WriteLine("InterruptAssessAndUpdate: Updating scheduleId " + item.scheduleid + " with seconds played " + item.SecondsPlayed, "ScheduleManager"); - } - catch - { - // If we have trouble getting it, then assume 0 to be safe - item.SecondsPlayed = 0; - } - } - - // Sort the interrupt layouts - CurrentInterruptSchedule.Sort(new ScheduleItemComparer(3600 - secondsIntoHour)); - CurrentInterruptSchedule.Reverse(); - - // Do we need to interrupt at this moment, or not - double percentageThroughPeriod = secondsIntoPeriod / 900.0; - int secondsShouldHaveInterrupted = Convert.ToInt32(Math.Floor(this._interruptState.TargetHourlyInterruption / 3600.0 * 900.0 * percentageThroughPeriod)); - int secondsSinceLastInterrupt = Convert.ToInt32((DateTime.Now - this._interruptState.LastInterruption).TotalSeconds); - - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptAssessAndUpdate: Target = " + this._interruptState.TargetHourlyInterruption - + ", Required = " + secondsShouldHaveInterrupted - + ", Interrupted = " + this._interruptState.SecondsInterrutedThisPeriod - + ", Last Interrupt = " + secondsSinceLastInterrupt - + ", Period Percent = " + percentageThroughPeriod) - , LogType.Audit.ToString()); - - // Interrupt if the seconds we've interrupted this hour so far is less than the seconds we - // should have interrupted. - if (!hasNotFulfilledSchedule) - { - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptAssessAndUpdate: No not-fulfilled schedules, Pause Pending."), LogType.Audit.ToString()); - OnInterruptPausePending?.Invoke(); - } - else if (Math.Floor(this._interruptState.SecondsInterrutedThisPeriod) < secondsShouldHaveInterrupted) - { - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptAssessAndUpdate: Interrupting."), LogType.Audit.ToString()); - OnInterruptNow?.Invoke(); - } - else - { - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptAssessAndUpdate: Pause Pending."), LogType.Audit.ToString()); - OnInterruptPausePending?.Invoke(); - } - } - } - - /// - /// Reset interrupt seconds if we've gone into a new hour - /// - private void InterruptResetSecondsIfNecessary() - { - if (this._interruptState.LastPlaytimeUpdate < TopOfPeriod()) - { - Debug.WriteLine("InterruptResetSecondsIfNecessary: LastPlaytimeUpdate in prior period, resetting play time.", "ScheduleManager"); - this._interruptState.SecondsInterrutedThisPeriod = 0; - } - - if (this._interruptState.LastPlaytimeUpdate < TopOfHour()) - { - Debug.WriteLine("InterruptResetSecondsIfNecessary: LastPlaytimeUpdate in prior hour, resetting hashes.", "ScheduleManager"); - this._interruptState.InterruptTracking.Clear(); - } - } - - /// - /// Mark an interrupt as having happened - /// - public void InterruptSetActive() - { - this._interruptState.LastInterruption = DateTime.Now; - - Debug.WriteLine("InterruptSetActive", "ScheduleManager"); - } - - /// - /// Record how many seconds we've just played from an event. - /// - /// - /// - public void InterruptRecordSecondsPlayed(int scheduleId, double seconds) - { - Debug.WriteLine("InterruptRecordSecondsPlayed: scheduleId = " + scheduleId + ", seconds = " + seconds, "ScheduleManager"); - - InterruptResetSecondsIfNecessary(); - - // Add to our overall interrupted seconds - this._interruptState.SecondsInterrutedThisPeriod += seconds; - - // Record the last play time as not - this._interruptState.LastPlaytimeUpdate = DateTime.Now; - - // Update our tracker with these details. - if (!this._interruptState.InterruptTracking.ContainsKey(scheduleId)) - { - this._interruptState.InterruptTracking.Add(scheduleId, 0); - } - - // Add new seconds to tracked seconds - this._interruptState.InterruptTracking[scheduleId] += seconds; - - // Log - Debug.WriteLine("InterruptRecordSecondsPlayed: Added " + seconds - + " seconds to eventId " + scheduleId + ", new total is " + this._interruptState.InterruptTracking[scheduleId], "ScheduleManager"); - } - - /// - /// Initialise interrupt state from disk - /// - private void InterruptInitState() - { - lock (_locker) - { - try - { - if (File.Exists(ApplicationSettings.Default.LibraryPath + @"\interrupt.json")) - { - this._interruptState = JsonConvert.DeserializeObject(File.ReadAllText(ApplicationSettings.Default.LibraryPath + @"\interrupt.json")); - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptInitState: Failed to read interrupt file. e = " + e.Message), LogType.Error.ToString()); - } - - // If we are still empty after loading, we should create an empty object - if (this._interruptState == null) - { - // Create a new empty object - this._interruptState = InterruptState.EmptyState(); - } - } - } - - /// - /// Persist state to disk - /// - private void InterruptPersistState() - { - // If the interrupt state is null for whatever reason, don't persist it to file - if (this._interruptState == null) - { - return; - } - - try - { - lock (_locker) - { - using (StreamWriter sw = new StreamWriter(ApplicationSettings.Default.LibraryPath + @"\interrupt.json", false, Encoding.UTF8)) - { - sw.Write(JsonConvert.SerializeObject(this._interruptState, Newtonsoft.Json.Formatting.Indented)); - } - } - } - catch (Exception e) - { - Trace.WriteLine(new LogMessage("ScheduleManager", "InterruptPersistState: Failed to update interrupt file. e = " + e.Message), LogType.Error.ToString()); - } - } - - /// - /// Return the top of the Hour - /// - /// - private DateTime TopOfHour() - { - return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 0, 0); - } - - /// - /// Get the top of this 15 minute period - /// - /// - private DateTime TopOfPeriod() - { - int currentMinute = DateTime.Now.Minute; - - if (currentMinute < 15) - { - return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 0, 0); - } - else if (currentMinute < 30) - { - return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 15, 0); - } - else if (currentMinute < 45) - { - return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 30, 0); - } - else - { - return new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 45, 0); - } - } - - #endregion } } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 75a2f456..b5c6fa19 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.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 * @@ -64,11 +64,6 @@ public partial class MainWindow : Window /// private Layout currentLayout; - /// - /// The Currently Running Interrupt Layout - /// - private Layout interruptLayout; - /// /// Are we in screensaver mode? /// @@ -411,59 +406,16 @@ private void MainForm_FormClosing(object sender, CancelEventArgs e) /// Handles the ScheduleChange event /// /// - /// - void ScheduleChangeEvent(ScheduleItem nextLayout, string mode) + void ScheduleChangeEvent(ScheduleItem nextLayout) { // We can only process 1 schedule change at a time. lock (_scheduleLocker) { - Debug.WriteLine("ScheduleChangeEvent: " + mode, "MainWindow"); - - // What mode have we received. - if (mode == "next") - { - Trace.WriteLine(new LogMessage("MainForm", + Trace.WriteLine(new LogMessage("MainForm", string.Format("ScheduleChangeEvent: Schedule Changing to Schedule {0}, Layout {1}", nextLayout.scheduleid, nextLayout.id)), LogType.Audit.ToString()); - // Issue a change to the next Layout - Dispatcher.Invoke(new Action(ChangeToNextLayout), nextLayout); - } - else if (mode == "interrupt-next") - { - Trace.WriteLine(new LogMessage("MainForm", - string.Format("ScheduleChangeEvent: Interrupt Schedule Changing to Schedule {0}, Layout {1}", nextLayout.scheduleid, nextLayout.id)), LogType.Audit.ToString()); - - // Issue a change to the next Layout - Dispatcher.Invoke(new Action(ChangeToNextInterruptLayout), nextLayout); - } - else if (mode == "interrupt") - { - // Pause the current layout, and start/resume the interrupt - Dispatcher.Invoke(new Action(Interrupt), nextLayout); - } - else if (mode == "interrupt-end") - { - // End the current interrupt layout and resume the current normal layout - if (this.interruptLayout != null && this.interruptLayout.IsRunning) - { - Dispatcher.Invoke(InterruptEnd); - } - } - else if (mode == "pause-pending") - { - // Set Pause Pending on the current interrupt layout - // so that when it finishes it will pause - if (this.interruptLayout != null) - { - this.interruptLayout.PausePending(); - } - - return; - } - else - { - Trace.WriteLine(new LogMessage("MainForm", string.Format("ScheduleChangeEvent: Unknown Mode {0}", mode)), LogType.Error.ToString()); - } + // Issue a change to the next Layout + Dispatcher.Invoke(new Action(ChangeToNextLayout), nextLayout); } } @@ -494,6 +446,16 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) { if (this.currentLayout != null) { + // Check to see if this Layout was a Layout Change Action that we can mark as being played + if (this.currentLayout.ScheduleItem.Override) + { + if (_schedule.NotifyLayoutActionFinished(this.currentLayout.ScheduleItem)) + { + Debug.WriteLine("ChangeToNextLayout: not changing this time, because the current layout finishing will result in a schedule change.", "MainWindow"); + return; + } + } + Debug.WriteLine("ChangeToNextLayout: stopping the current Layout", "MainWindow"); this.currentLayout.Stop(); @@ -555,11 +517,14 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) { Trace.WriteLine(new LogMessage("MainForm", "ChangeToNextLayout: Layout Change to " + scheduleItem.layoutFile + " failed. Exception raised was: " + ex.Message), LogType.Error.ToString()); + // Store the active layout count, so that we can remove this one that failed and still see if there is another to try + int activeLayouts = _schedule.ActiveLayouts; + // We could not prepare or start this Layout, so we ought to remove it from the Schedule. _schedule.RemoveLayout(scheduleItem); // Do we have more than one Layout in our Schedule which we can try? - if (_schedule.ActiveLayouts > 1) + if (activeLayouts > 1) { _schedule.NextLayout(); } @@ -590,74 +555,6 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) } } - /// - /// Change to the next layout - /// - /// - private void ChangeToNextInterruptLayout(ScheduleItem scheduleItem) - { - Debug.WriteLine("ChangeToNextInterruptLayout: called", "MainWindow"); - - try - { - // Destroy the Current Layout - try - { - if (this.interruptLayout != null) - { - Debug.WriteLine("ChangeToNextInterruptLayout: stopping the current Layout", "MainWindow"); - - this.interruptLayout.Stop(); - - DestroyLayout(this.interruptLayout); - } - } - catch (Exception e) - { - // Force collect all controls - this.Scene.Children.Clear(); - - Trace.WriteLine(new LogMessage("MainForm", "ChangeToNextInterruptLayout: Destroy Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); - throw e; - } - - // Prepare the next layout - try - { - this.interruptLayout = PrepareLayout(scheduleItem); - - // We have loaded a layout background and therefore are no longer showing the splash screen - // Remove the Splash Screen Image - RemoveSplashScreen(); - - // Start the Layout. - StartLayout(this.interruptLayout); - } - catch (Exception e) - { - DestroyLayout(this.currentLayout); - Trace.WriteLine(new LogMessage("MainForm", "ChangeToNextInterruptLayout: Prepare Layout Failed. Exception raised was: " + e.Message), LogType.Info.ToString()); - throw; - } - } - catch - { - // We have not been able to load the interrupt layout, move to the next if we can - if (_schedule.ActiveInterruptLayouts > 1) - { - Debug.WriteLine("ChangeToNextInterruptLayout: More than one interrupt Layout, calling Next", "MainWindow"); - _schedule.NextLayout(); - } - else - { - // we assume here that the prior steps catch statements have tidied up this Layout - Debug.WriteLine("ChangeToNextInterruptLayout: cannot start the only interrupt Layout, calling SetInterruptUnableToPlayAndEnd", "MainWindow"); - - _schedule.SetInterruptUnableToPlayAndEnd(); - } - } - } - /// /// Start a Layout /// @@ -673,12 +570,7 @@ private void StartLayout(Layout layout) this.Scene.Children.Add(layout); // Start - if (layout.IsPaused) - { - Debug.WriteLine("StartLayout: Resuming paused Layout", "MainWindow"); - layout.Resume(); - } - else if (!layout.IsRunning) + if (!layout.IsRunning) { Debug.WriteLine("StartLayout: Starting Layout", "MainWindow"); layout.Start(); @@ -715,107 +607,6 @@ private void StartLayout(Layout layout) } } - /// - /// Interrupt - /// - private void Interrupt(ScheduleItem scheduleItem) - { - Debug.WriteLine("Interrupt: " + scheduleItem.scheduleid, "MainWindow"); - - try - { - if (this.currentLayout != null && this.currentLayout.IsRunning) - { - Debug.WriteLine("Interrupt: Pausing current normal layout: " + this.currentLayout.ScheduleId, "MainWindow"); - - this.currentLayout.Pause(); - - Debug.WriteLine("Interrupt: Paused, removing from Scene", "MainWindow"); - - this.Scene.Children.Remove(this.currentLayout); - } - - if (this.interruptLayout == null) - { - // Prepare the interrupt Layout - this.interruptLayout = PrepareLayout(scheduleItem); - } - else if (this.interruptLayout.ScheduleId != scheduleItem.scheduleid) - { - this.interruptLayout.Stop(); - DestroyLayout(this.interruptLayout); - - this.interruptLayout = PrepareLayout(scheduleItem); - } - - StartLayout(this.interruptLayout); - - // We are interrupting - this._schedule.SetInterrupting(); - - // Are we expired? - if (this.interruptLayout.IsExpired) - { - // The interrupt Layout is expired, so we ask for the next one - Debug.WriteLine("Interrupt: Current Interrupt is Expired, so move on", "MainWindow"); - - this._schedule.NextLayout(); - } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm", "Interrupt: Exception raised was: " + ex.Message), LogType.Error.ToString()); - } - } - - /// - /// Interrupt End - /// - private void InterruptEnd() - { - Debug.WriteLine("InterruptEnd: Ending...", "MainWindow"); - - try - { - // Stop the current interrupt - if (this.interruptLayout != null && this.interruptLayout.IsRunning) - { - Debug.WriteLine("InterruptEnd: Pausing current interrupt", "MainWindow"); - - this.interruptLayout.Pause(); - - Debug.WriteLine("InterruptEnd: Removing from the scene", "MainWindow"); - - this.Scene.Children.Remove(this.interruptLayout); - } - - if (this.currentLayout == null || !this.currentLayout.IsPaused) - { - // Call schedule change - this._schedule.NextLayout(); - } - else - { - StartLayout(this.currentLayout); - } - } - catch (Exception ex) - { - Trace.WriteLine(new LogMessage("MainForm", "InterruptEnd: Exception raised was: " + ex.Message), LogType.Error.ToString()); - } - } - - /// - /// Report current Layout Play Duration. - /// - /// - /// - /// - private void CurrentLayout_OnReportLayoutPlayDurationEvent(int scheduleId, int layoutId, double duration) - { - this._schedule.CurrentLayout_OnReportLayoutPlayDurationEvent(scheduleId, layoutId, duration); - } - /// /// Expire the Splash Screen /// @@ -863,7 +654,6 @@ private Layout PrepareLayout(ScheduleItem scheduleItem) Schedule = _schedule }; layout.loadFromFile(scheduleItem); - layout.OnReportLayoutPlayDurationEvent += CurrentLayout_OnReportLayoutPlayDurationEvent; return layout; } catch (IOException) @@ -953,7 +743,6 @@ private void DestroyLayout(Layout layout) Debug.WriteLine("DestoryLayout: Destroying Layout", "MainForm"); layout.Remove(); - layout.OnReportLayoutPlayDurationEvent -= CurrentLayout_OnReportLayoutPlayDurationEvent; this.Scene.Children.Remove(layout); } diff --git a/OptionsForm.xaml.cs b/OptionsForm.xaml.cs index a91208f6..ba2ca187 100644 --- a/OptionsForm.xaml.cs +++ b/OptionsForm.xaml.cs @@ -505,7 +505,6 @@ private static async Task CheckCode(string UserCode, string DeviceCode) if (json.ContainsKey("cmsAddress")) { - // TODO: we are done! ApplicationSettings.Default.ServerUri = json["cmsAddress"].ToString(); ApplicationSettings.Default.ServerKey = json["cmsKey"].ToString(); diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 9ec8b7b2..a5358307 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 2020")] +[assembly: AssemblyCopyright("Copyright © Xibo Signage Ltd 2021")] [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("2.256.0.7")] -[assembly: AssemblyFileVersion("2.256.0.7")] +[assembly: AssemblyVersion("2.257.1.0")] +[assembly: AssemblyFileVersion("2.257.1.0")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 8379779c..51d80020 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.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 * @@ -85,15 +85,8 @@ public partial class Layout : UserControl // Layout state public bool IsRunning { get; set; } - public bool IsPaused { get; set; } public bool IsExpired { get; private set; } - // Interrupts - private bool isPausePending = false; - - public delegate void OnReportLayoutPlayDuration(int scheduleId, int layoutId, double duration); - public event OnReportLayoutPlayDuration OnReportLayoutPlayDurationEvent; - /// /// Layout /// @@ -362,61 +355,6 @@ public void Start() // We are running IsRunning = true; - IsPaused = false; - } - - /// - /// Pause this Layout - /// - public void Pause() - { - // Pause each Region - foreach (Region region in _regions) - { - region.Pause(); - } - - // Pause no-longer pending - isPausePending = false; - IsPaused = true; - - // Close and dispatch any stat records - double duration = StatManager.Instance.LayoutStop(UniqueId, ScheduleId, _layoutId, this.isStatEnabled); - - // Report Play Duration - if (this.isInterrupt) - { - OnReportLayoutPlayDurationEvent?.Invoke(ScheduleId, _layoutId, duration); - } - } - - /// - /// Set Pause Pending, so that next expiry we pause. - /// - public void PausePending() - { - isPausePending = true; - - foreach(Region region in _regions) - { - region.PausePending(); - } - } - - /// - /// Resume this Layout - /// - public void Resume() - { - StatManager.Instance.LayoutStart(UniqueId, ScheduleId, _layoutId); - - // Resume each region - foreach (Region region in _regions) - { - region.Resume(this.isInterrupt); - } - - IsPaused = false; } /// @@ -427,14 +365,9 @@ public void Stop() // Stat stop double duration = StatManager.Instance.LayoutStop(UniqueId, ScheduleId, _layoutId, this.isStatEnabled); - // If we are an interrupt layout, then report our duration. - if (this.isInterrupt) - { - OnReportLayoutPlayDurationEvent?.Invoke(ScheduleId, this._layoutId, duration); - } + // Record final duration of this layout in memory cache + CacheManager.Instance.RecordLayoutDuration(_layoutId, (int)Math.Ceiling(duration)); - // Stop - IsPaused = false; IsRunning = false; } @@ -485,13 +418,6 @@ private void Region_DurationElapsedEvent() return; } - // If we are paused, don't do anything - if (IsPaused) - { - Debug.WriteLine("Region_DurationElapsedEvent: On Paused Layout, ignoring.", "Layout"); - return; - } - bool isExpired = true; // Check the other regions to see if they are also expired. @@ -536,12 +462,6 @@ private void Region_DurationElapsedEvent() private void Region_MediaExpiredEvent() { Trace.WriteLine(new LogMessage("Region", "MediaExpiredEvent: Media Elapsed"), LogType.Audit.ToString()); - - // Are we supposed to be pausing? - if (this.isPausePending) - { - Schedule.SetInterruptMediaPlayed(); - } } /// diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index 79bb0d56..40e9f4c6 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -326,7 +326,7 @@ public void TransitionOut() To = 0, Duration = TimeSpan.FromMilliseconds(duration) }; - animation.Completed += Animation_Completed; + animation.Completed += Stop_Animation_Completed; BeginAnimation(OpacityProperty, animation); break; } @@ -343,8 +343,21 @@ public void TransitionOut() /// /// /// - private void Animation_Completed(object sender, EventArgs e) + private void Start_Animation_Completed(object sender, EventArgs e) { + // Do we need to do anything in here? + Debug.WriteLine("In", "Start_Animation_Completed"); + } + + /// + /// Animation completed + /// + /// + /// + private void Stop_Animation_Completed(object sender, EventArgs e) + { + Debug.WriteLine("In", "Stop_Animation_Completed"); + // Indicate we have stopped (only once) if (!this._stopped) { @@ -364,13 +377,25 @@ private void FlyAnimation(string direction, double duration, bool isInbound) // We might not need both of these, but we add them just in case we have a mid-way compass point var trans = new TranslateTransform(); - DoubleAnimation doubleAnimationX = new DoubleAnimation(); - doubleAnimationX.Duration = TimeSpan.FromMilliseconds(duration); - doubleAnimationX.Completed += Animation_Completed; + DoubleAnimation doubleAnimationX = new DoubleAnimation + { + Duration = TimeSpan.FromMilliseconds(duration) + }; + DoubleAnimation doubleAnimationY = new DoubleAnimation + { + Duration = TimeSpan.FromMilliseconds(duration) + }; - DoubleAnimation doubleAnimationY = new DoubleAnimation(); - doubleAnimationY.Duration = TimeSpan.FromMilliseconds(duration); - doubleAnimationY.Completed += Animation_Completed; + if (isInbound) + { + doubleAnimationX.Completed += Start_Animation_Completed; + doubleAnimationY.Completed += Start_Animation_Completed; + } + else + { + doubleAnimationX.Completed += Stop_Animation_Completed; + doubleAnimationY.Completed += Stop_Animation_Completed; + } // Get the viewable window width and height int screenWidth = options.PlayerWidth; @@ -408,7 +433,7 @@ private void FlyAnimation(string direction, double duration, bool isInbound) doubleAnimationY.From = top; } - BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); break; case "NE": @@ -426,7 +451,6 @@ private void FlyAnimation(string direction, double duration, bool isInbound) trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); - RenderTransform = trans; break; case "E": @@ -447,7 +471,7 @@ private void FlyAnimation(string direction, double duration, bool isInbound) } - BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); + trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); break; case "SE": @@ -464,7 +488,6 @@ private void FlyAnimation(string direction, double duration, bool isInbound) trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); - RenderTransform = trans; break; case "S": @@ -477,7 +500,7 @@ private void FlyAnimation(string direction, double duration, bool isInbound) doubleAnimationX.From = -top; } - BeginAnimation(TranslateTransform.YProperty, doubleAnimationX); + trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationX); break; case "SW": @@ -494,7 +517,6 @@ private void FlyAnimation(string direction, double duration, bool isInbound) trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); - RenderTransform = trans; break; case "W": @@ -508,7 +530,6 @@ private void FlyAnimation(string direction, double duration, bool isInbound) } trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); - RenderTransform = trans; break; case "NW": @@ -525,9 +546,11 @@ private void FlyAnimation(string direction, double duration, bool isInbound) trans.BeginAnimation(TranslateTransform.XProperty, doubleAnimationX); trans.BeginAnimation(TranslateTransform.YProperty, doubleAnimationY); - RenderTransform = trans; break; } + + // Set this Media's render transform + RenderTransform = trans; } } } diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index c3f83370..e87205ad 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -1,10 +1,29 @@ -using System; +/** + * 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 System; using System.Diagnostics; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Xml; -using XiboClient.Logic; using XiboClient.Stats; namespace XiboClient.Rendering @@ -24,16 +43,6 @@ public partial class Region : UserControl /// public bool IsExpired = false; - /// - /// Is this region paused? - /// - private bool _isPaused = false; - - /// - /// Is Pause Pending? - /// - private bool IsPausePending = false; - /// /// This Regions zIndex /// @@ -56,7 +65,6 @@ public partial class Region : UserControl private bool _sizeResetRequired; private bool _dimensionsSet = false; private int _audioSequence; - private double _currentPlaytime; /// /// Event to indicate that this Region's duration has elapsed @@ -849,20 +857,6 @@ private void media_DurationElapsedEvent(int filesPlayed) return; } - // If we are now paused, we don't start the next media - if (this._isPaused) - { - Debug.WriteLine("DurationElapsedEvent: Paused, therefore we don't StartNext", "Region"); - return; - } - - // If Pause Pending, then stop here as we will be removed - if (IsPausePending) - { - Debug.WriteLine("DurationElapsedEvent: Pause Pending, therefore we don't StartNext", "Region"); - return; - } - // TODO: // Animate out at this point if we need to // the result of the animate out complete event should then move us on. @@ -917,68 +911,6 @@ private void SetDimensions(Point location, Size size) SetDimensions((int)location.X, (int)location.Y, (int)size.Width, (int)size.Height); } - /// - /// Is Pause Pending? - /// - public void PausePending() - { - this.IsPausePending = true; - } - - /// - /// Pause this Layout - /// - public void Pause() - { - if (this.currentMedia != null) - { - // Store the current playtime of this Region. - this._currentPlaytime = this.currentMedia.CurrentPlaytime(); - - // Stop and remove the current media. - StopMedia(this.currentMedia, true); - - // Remove it. - this.currentMedia = null; - - Debug.WriteLine("Pause: paused Region, current Playtime is " + this._currentPlaytime, "Region"); - } - - // Paused - this._isPaused = true; - this.IsPausePending = false; - } - - /// - /// Resume this Layout - /// - public void Resume(bool isInterrupt) - { - // If we are an interrupt, we should skip on to the next item - // and if there is only 1 item, we should replay it. - // if we are a normal layout, then we resume the current one. - if (isInterrupt) - { - if (this.options.mediaNodes.Count <= 1) - { - this.currentSequence--; - } - - // Resume the current media item - StartNext(this._currentPlaytime); - } - else - { - // We have to dial back the current position here, because start next will straight away increment it - this.currentSequence--; - - // Resume the current media item - StartNext(0); - } - - this._isPaused = false; - } - /// /// Clears the Region of anything that it shouldnt still have... /// called when Destroying a Layout and when Removing an Overlay diff --git a/Stats/StatManager.cs b/Stats/StatManager.cs index 57a8cef3..8af74c37 100644 --- a/Stats/StatManager.cs +++ b/Stats/StatManager.cs @@ -199,6 +199,12 @@ public void LayoutStart(Guid uniqueId, int scheduleId, int layoutId) LayoutId = layoutId }; + // Start location + if (ApplicationSettings.Default.IsRecordGeoLocationOnProofOfPlay) + { + AnnotateWithLocation(stat); + } + this.proofOfPlay.Add(key, stat); } } @@ -231,7 +237,10 @@ public double LayoutStop(Guid uniqueId, int scheduleId, int layoutId, bool statE duration = (stat.To - stat.From).TotalSeconds; // GeoLocation - AnnotateWithLocation(stat, duration); + if (ApplicationSettings.Default.IsRecordGeoLocationOnProofOfPlay) + { + AnnotateWithLocationUpdate(stat, duration); + } if (ApplicationSettings.Default.StatsEnabled && statEnabled) { @@ -272,6 +281,12 @@ public void WidgetStart(int scheduleId, int layoutId, string widgetId) WidgetId = widgetId }; + // Start location + if (ApplicationSettings.Default.IsRecordGeoLocationOnProofOfPlay) + { + AnnotateWithLocation(stat); + } + this.proofOfPlay.Add(key, stat); } } @@ -307,7 +322,10 @@ public double WidgetStop(int scheduleId, int layoutId, string widgetId, bool sta duration = (stat.To - stat.From).TotalSeconds; // GeoLocation - AnnotateWithLocation(stat, duration); + if (ApplicationSettings.Default.IsRecordGeoLocationOnProofOfPlay) + { + AnnotateWithLocationUpdate(stat, duration); + } if (ApplicationSettings.Default.StatsEnabled && statEnabled) { @@ -329,8 +347,7 @@ public double WidgetStop(int scheduleId, int layoutId, string widgetId, bool sta /// Annotate a stat record with an engagement /// /// - /// - private void AnnotateWithLocation(Stat stat, double duration) + private void AnnotateWithLocation(Stat stat) { // Do we have any engagements to record? if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) @@ -338,14 +355,47 @@ private void AnnotateWithLocation(Stat stat, double duration) // Annotate our stat with the current geolocation Engagement engagement = new Engagement { - Tag = "LOCATION:" + ClientInfo.Instance.CurrentGeoLocation.Latitude + ":" + ClientInfo.Instance.CurrentGeoLocation.Longitude, - Duration = duration, + Tag = "LOCATION:" + ClientInfo.Instance.CurrentGeoLocation.Latitude + "," + ClientInfo.Instance.CurrentGeoLocation.Longitude, + Duration = 0, Count = 1 }; stat.Engagements.Add("LOCATION", engagement); } } + /// + /// Upate a stat record with an engagement + /// + /// + /// + private void AnnotateWithLocationUpdate(Stat stat, double duration) + { + if (stat.Engagements.ContainsKey("LOCATION")) + { + // Update the existing tag + stat.Engagements["LOCATION"].Duration = duration; + if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) + { + stat.Engagements["LOCATION"].Tag += "|" + ClientInfo.Instance.CurrentGeoLocation.Latitude + "," + ClientInfo.Instance.CurrentGeoLocation.Longitude; + } + } + else + { + // New one + if (ClientInfo.Instance.CurrentGeoLocation != null && !ClientInfo.Instance.CurrentGeoLocation.IsUnknown) + { + // Annotate our stat with the current geolocation + Engagement engagement = new Engagement + { + Tag = "LOCATION:" + ClientInfo.Instance.CurrentGeoLocation.Latitude + "," + ClientInfo.Instance.CurrentGeoLocation.Longitude, + Duration = duration, + Count = 1 + }; + stat.Engagements.Add("LOCATION", engagement); + } + } + } + /// /// Records a stat record /// diff --git a/XiboClient.csproj b/XiboClient.csproj index 11587180..127b2ea1 100644 --- a/XiboClient.csproj +++ b/XiboClient.csproj @@ -126,7 +126,6 @@ - @@ -136,7 +135,6 @@ - diff --git a/default.config.xml b/default.config.xml index 91833116..f16da1b5 100644 --- a/default.config.xml +++ b/default.config.xml @@ -54,4 +54,5 @@ 0 false individual + false \ No newline at end of file