diff --git a/Adspace/Ad.cs b/Adspace/Ad.cs index f2304d5f..d00e4e5b 100644 --- a/Adspace/Ad.cs +++ b/Adspace/Ad.cs @@ -18,8 +18,6 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ -using Flurl; -using Flurl.Http; using GeoJSON.Net.Contrib.MsSqlSpatial; using GeoJSON.Net.Feature; using GeoJSON.Net.Geometry; @@ -109,19 +107,6 @@ public string GetFileName() } } - /// - /// Download this ad - /// - public void Download() - { - // We should download it. - string fileName = GetFileName(); - new Url(Url).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName).ContinueWith(t => - { - CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); - }, System.Threading.Tasks.TaskContinuationOptions.OnlyOnRanToCompletion); - } - /// /// Set whether or not this GeoSchedule is active. /// diff --git a/Adspace/ExchangeManager.cs b/Adspace/ExchangeManager.cs index db0b5a9e..7c672579 100644 --- a/Adspace/ExchangeManager.cs +++ b/Adspace/ExchangeManager.cs @@ -21,12 +21,10 @@ using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; -using Org.BouncyCastle.Crypto.Engines; using Swan; using System; using System.Collections.Generic; using System.Diagnostics; -using System.EnterpriseServices; using System.Linq; using System.Threading.Tasks; using System.Xml; @@ -42,12 +40,14 @@ class ExchangeManager private readonly object buffetLock = new object(); private bool isActive; private bool isNewPrefetchAdded = false; + private bool isUnwrapping = false; private DateTime lastFillDate; private DateTime lastPrefetchDate; private List prefetchUrls = new List(); private List adBuffet = new List(); private Dictionary lastUnwrapRateLimits = new Dictionary(); private Dictionary lastUnwrapDates = new Dictionary(); + private List creativesDownloading = new List(); public int ShareOfVoice { get; private set; } = 0; public int AverageAdDuration { get; private set; } = 0; @@ -159,7 +159,7 @@ public Ad GetAd(double width, double height) } catch { - Trace.WriteLine(new LogMessage("ExchangeManager", "GetAd: no available ad returned while unwrapping"), LogType.Error.ToString()); + Trace.WriteLine(new LogMessage("ExchangeManager", "GetAd: no available ad returned while unwrapping"), LogType.Info.ToString()); throw new AdspaceNoAdException("No ad returned"); } @@ -167,7 +167,7 @@ public Ad GetAd(double width, double height) if (!ad.IsGeoActive(ClientInfo.Instance.CurrentGeoLocation)) { ReportError(ad.ErrorUrls, 408); - adBuffet.Remove(ad); + RemoveFromBuffet(ad); throw new AdspaceNoAdException("Outside geofence"); } @@ -183,7 +183,7 @@ public Ad GetAd(double width, double height) else { ReportError(ad.ErrorUrls, 200); - adBuffet.Remove(ad); + RemoveFromBuffet(ad); throw new AdspaceNoAdException("Type not recognised"); } @@ -191,28 +191,38 @@ public Ad GetAd(double width, double height) if (width / height != ad.AspectRatio) { ReportError(ad.ErrorUrls, 203); - adBuffet.Remove(ad); + RemoveFromBuffet(ad); throw new AdspaceNoAdException("Dimensions invalid"); } - // TODO: check fault status - // Check to see if the file is already there, and if not, download it. if (!CacheManager.Instance.IsValidPath(ad.GetFileName())) { - Task.Run(() => ad.Download()); + DownloadAd(ad); // Don't show it this time - adBuffet.Remove(ad); + RemoveFromBuffet(ad); throw new AdspaceNoAdException("Creative pending download"); } // We've converted it into a play - adBuffet.Remove(ad); + RemoveFromBuffet(ad); return ad; } + /// + /// Removes an ad from the buffet, respecting the buffet lock + /// + /// + private void RemoveFromBuffet(Ad ad) + { + lock (buffetLock) + { + adBuffet.Remove(ad); + } + } + /// /// Get an available ad /// @@ -293,12 +303,7 @@ public void Prefetch() string fileName = "axe_" + creative.GetValue(idProp).ToString(); if (!CacheManager.Instance.IsValidPath(fileName)) { - // We should download it. - new Url(fetchUrl).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName).ContinueWith(t => - { - CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); - }, - TaskContinuationOptions.OnlyOnRanToCompletion); + DownloadAd(fetchUrl, fileName); } } } @@ -313,12 +318,7 @@ public void Prefetch() string fileName = "axe_" + fetchUrl.Split('/').Last(); if (!CacheManager.Instance.IsValidPath(fileName)) { - // We should download it. - new Url(fetchUrl).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName).ContinueWith(t => - { - CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); - }, - TaskContinuationOptions.OnlyOnRanToCompletion); + DownloadAd(fetchUrl, fileName); } } } @@ -768,7 +768,7 @@ private List Request(Url url, Ad wrappedAd) // Download if necessary if (!CacheManager.Instance.IsValidPath(ad.GetFileName())) { - Task.Run(() => ad.Download()); + DownloadAd(ad); } // Ad this to our list @@ -799,8 +799,16 @@ private List Request(Url url, Ad wrappedAd) /// private void UnwrapAds() { + if (isUnwrapping) + { + // Don't queue, just unwrap + return; + } + lock (buffetLock) { + isUnwrapping = true; + // Keep a list of ads we add List unwrappedAds = new List(); @@ -844,6 +852,8 @@ private void UnwrapAds() // Add in any new ones we've got as a result adBuffet.AddRange(unwrappedAds); unwrappedAds.Clear(); + + isUnwrapping = false; } } @@ -877,6 +887,46 @@ private void ReportError(List urls, int errorCode) } } + private void DownloadAd(string url, string fileName) + { + lock (creativesDownloading) + { + if (creativesDownloading.Contains(fileName)) + { + LogMessage.Info("ExchangeManager", "DownloadAd", "Already downloading " + fileName); + return; + } + + // Not downloading yet, so do it now + creativesDownloading.Add(fileName); + + // We use a task for this so that it happens in the background + try + { + new Url(url).DownloadFileAsync(ApplicationSettings.Default.LibraryPath, fileName).ContinueWith(t => + { + // Completed successfully, so add to the cache manager and remove from download queue + CacheManager.Instance.Add(fileName, CacheManager.Instance.GetMD5(fileName)); + creativesDownloading.Remove(fileName); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } + catch (Exception e) + { + LogMessage.Error("ExchangeManager", "DownloadAd", "Failed to download " + fileName + ", e: " + e.Message); + CacheManager.Instance.Remove(fileName); + } + } + } + + /// + /// Download an ad + /// + /// + private void DownloadAd(Ad ad) + { + DownloadAd(ad.Url, ad.GetFileName()); + } + private void SetUnwrapRateThreshold(string partner, int seconds) { if (!string.IsNullOrEmpty(partner)) diff --git a/Logic/ApplicationSettings.cs b/Logic/ApplicationSettings.cs index 67964fb4..0734212f 100644 --- a/Logic/ApplicationSettings.cs +++ b/Logic/ApplicationSettings.cs @@ -52,7 +52,7 @@ private static readonly Lazy /// private List ExcludedProperties; - public string ClientVersion { get; } = "3 R306.2"; + public string ClientVersion { get; } = "3 R306.3"; public string Version { get; } = "6"; public int ClientCodeVersion { get; } = 306; diff --git a/Logic/Schedule.cs b/Logic/Schedule.cs index 16223e6f..00735433 100644 --- a/Logic/Schedule.cs +++ b/Logic/Schedule.cs @@ -557,6 +557,17 @@ public int ActiveLayouts } } + /// + /// The number of active adspace exchange events + /// + public int ActiveAdspaceExchangeEvents + { + get + { + return _layoutSchedule.FindAll(item => item.IsAdspaceExchange).Count; + } + } + /// /// A layout file has changed /// diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 00b1f982..5014151f 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -557,16 +557,24 @@ private void ChangeToNextLayout(ScheduleItem scheduleItem) } catch (Exception ex) { - 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); + if (scheduleItem.IsAdspaceExchange) + { + LogMessage.Audit("MainForm", "ChangeToNextLayout", "No ad to show, e: " + ex.Message); + } + else + { + LogMessage.Info("MainForm", "ChangeToNextLayout", "Layout Change to " + scheduleItem.layoutFile + " failed. Exception raised was: " + ex.Message); + + // 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 (activeLayouts > 1) + // and make sure they aren't solely AXE + if (activeLayouts > 1 && activeLayouts > _schedule.ActiveAdspaceExchangeEvents) { _schedule.NextLayout(); } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index b1417f13..106b2fe8 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.306.0.2")] -[assembly: AssemblyFileVersion("3.306.0.2")] +[assembly: AssemblyVersion("3.306.0.3")] +[assembly: AssemblyFileVersion("3.306.0.3")] [assembly: Guid("3bd467a4-4ef9-466a-b156-a79c13a863f7")] diff --git a/Rendering/Layout.xaml.cs b/Rendering/Layout.xaml.cs index 581d2083..8e7096f8 100644 --- a/Rendering/Layout.xaml.cs +++ b/Rendering/Layout.xaml.cs @@ -587,6 +587,7 @@ public void LoadFromAd(ScheduleItem scheduleItem, Ad ad) // Set our impression URLs which we will call on stop. _regions[0].SetAdspaceExchangeImpressionUrls(ad.ImpressionUrls); + _regions[0].SetAdspaceExchangeErrorUrls(ad.ErrorUrls); } /// diff --git a/Rendering/Media.xaml.cs b/Rendering/Media.xaml.cs index 09fdef84..5274b6f5 100644 --- a/Rendering/Media.xaml.cs +++ b/Rendering/Media.xaml.cs @@ -118,6 +118,11 @@ public partial class Media : UserControl /// public bool StatsEnabled { get; private set; } + /// + /// Did this media item fail to play? + /// + public bool IsFailedToPlay { get; protected set; } + /// /// Media Object /// @@ -132,6 +137,9 @@ public Media(MediaOptions options) ScheduleId = options.scheduleId; LayoutId = options.layoutId; StatsEnabled = options.isStatEnabled; + + // Start us off in a good state + IsFailedToPlay = false; } /// diff --git a/Rendering/Region.xaml.cs b/Rendering/Region.xaml.cs index 0af26bfc..226312fe 100644 --- a/Rendering/Region.xaml.cs +++ b/Rendering/Region.xaml.cs @@ -18,10 +18,13 @@ * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ +using Flurl; +using Flurl.Http; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; +using System.Security.Policy; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Xml; @@ -99,6 +102,11 @@ public partial class Region : UserControl /// private List adspaceExchangeImpressionUrls = new List(); + /// + /// Ad list of error ulrs to call on stop. + /// + private List adspaceExchangeErrorUrls = new List(); + /// /// Is this an adspace exchange region? /// @@ -696,7 +704,39 @@ private void StopMedia(Media media) try { // Close the stat record - StatManager.Instance.WidgetStop(media.ScheduleId, media.LayoutId, media.Id, media.StatsEnabled, adspaceExchangeImpressionUrls); + if (media.IsFailedToPlay) + { + StatManager.Instance.WidgetClearFailed(media.ScheduleId, media.LayoutId, media.Id); + + if (isAdspaceExchange) + { + foreach (string url in adspaceExchangeErrorUrls) + { + try + { + // Macros + string uri = url + .Replace("[TIMESTAMP]", "" + DateTime.Now.ToString("o", System.Globalization.CultureInfo.InvariantCulture)) + .Replace("[ERRORCODE]", "201"); + + // Call the URL + new Flurl.Url(uri).WithTimeout(10).GetAsync().ContinueWith(t => + { + LogMessage.Error("Region", "StopMedia", "failed to report error to " + uri); + }, + TaskContinuationOptions.OnlyOnFaulted); + } + catch + { + LogMessage.Error("Region", "StopMedia", "failed to report error"); + } + } + } + } + else + { + StatManager.Instance.WidgetStop(media.ScheduleId, media.LayoutId, media.Id, media.StatsEnabled, adspaceExchangeImpressionUrls); + } // Media Stopped Event removes the media from the scene media.MediaStoppedEvent += Media_MediaStoppedEvent; @@ -903,5 +943,14 @@ public void SetAdspaceExchangeImpressionUrls(List urls) isAdspaceExchange = true; adspaceExchangeImpressionUrls = urls; } + + /// + /// Set any adspace exchange error urls. + /// + /// + public void SetAdspaceExchangeErrorUrls(List urls) + { + adspaceExchangeErrorUrls = urls; + } } } diff --git a/Rendering/Video.cs b/Rendering/Video.cs index 4cc0f6ca..b3a4af60 100644 --- a/Rendering/Video.cs +++ b/Rendering/Video.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 * @@ -110,6 +110,9 @@ private void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e) // Add this to a temporary blacklist so that we don't repeat it too quickly CacheManager.Instance.AddUnsafeItem(UnsafeItemType.Media, UnsafeFaultCodes.VideoUnexpected, LayoutId, Id, "Video Failed: " + e.ErrorException.Message, 120); + // Set as failed to play + IsFailedToPlay = true; + // Expire SignalElapsedEvent(); } diff --git a/Stats/StatManager.cs b/Stats/StatManager.cs index d5142aa9..9ab7f5d6 100644 --- a/Stats/StatManager.cs +++ b/Stats/StatManager.cs @@ -303,6 +303,19 @@ public void WidgetStart(int scheduleId, int layoutId, string widgetId) } } + public void WidgetClearFailed(int scheduleId, int layoutId, string widgetId) + { + lock (_locker) + { + // Record we expect to already be open in the Dictionary + string key = scheduleId + "-" + layoutId + "-" + widgetId; + + LogMessage.Info("StatManager", "WidgetClearFailed", "Removing failed widget: " + key); + + this.proofOfPlay.Remove(key); + } + } + /// /// Widget Stop Event ///