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
///