From 2e4603cddae3b08169cd64bbd1943620bd13caf4 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Mon, 30 Mar 2026 22:36:15 +0200 Subject: [PATCH 1/3] f B: Add GetConfiguration() to Unity AirConsole plugin Parse configuration from OnReady message into _configuration field, reset it in ResetCaches, and expose via GetConfiguration() returning a JToken with supportedVideoFormats, transparentVideoSupported, unityVideoSupported, and graphicsQualityTier. Adds EditMode test. ENG-74 Task 4 --- .../AirConsole/scripts/Runtime/AirConsole.cs | 26 ++++++++++ .../scripts/Tests/EditMode/AirConsoleTests.cs | 51 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index bda49b8..afac84f 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -658,6 +658,27 @@ public long GetServerTime() { return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds + _server_time_offset; } + /// + /// Returns the platform capability configuration. + /// Use this to branch on capabilities instead of platform or partner names. + /// Can only be called after OnReady. + /// + /// + /// A JToken containing: + /// supportedVideoFormats (string[]) - e.g. ["vp9","h264","vp8"] + /// transparentVideoSupported (bool) + /// unityVideoSupported (bool) + /// graphicsQualityTier (string) - "low", "medium", or "high" + /// Returns null if not yet received. + /// + public JToken GetConfiguration() { + if (!IsAirConsoleUnityPluginReady()) { + throw new NotReadyException(); + } + + return _configuration; + } + /// /// Request that all devices return to the AirConsole store. /// @@ -1533,6 +1554,9 @@ private void OnReady(JObject msg) { _devices.Add(assign); } + // parse configuration + _configuration = msg["configuration"]; + _receivedReady = true; if (onReady != null) { @@ -1629,6 +1653,7 @@ private void ResetCaches(Action taskToQueueAfterClear) { _server_time_offset = 0; _location = null; _translations = null; + _configuration = null; _receivedReady = false; // Reset safe area @@ -1990,6 +2015,7 @@ internal static bool IsAndroidOrEditor { private int _server_time_offset; private string _location; private Dictionary _translations; + private JToken _configuration; private readonly List _players = new(); private readonly Queue eventQueue = new(); private bool _safeAreaWasSet; diff --git a/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs b/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs index f1b3961..a80ade6 100644 --- a/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs +++ b/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs @@ -82,6 +82,50 @@ public IEnumerator SetSafeArea_WithInvalidMessage_ExceptionIsRaised() { yield return null; } + [UnityTest] + [Timeout(300)] + public IEnumerator GetConfiguration_AfterReady_ReturnsConfiguration() { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { + Assert.Inconclusive("This test requires an Android build target"); + } + + bool testIsDone = false; + JObject configuration = JObject.FromObject(new { + supportedVideoFormats = new[] { "vp9", "h264", "vp8" }, + transparentVideoSupported = true, + unityVideoSupported = true, + graphicsQualityTier = "high" + }); + JObject readyMessage = JObject.FromObject(new { + action = "ready", + code = "test123", + device_id = 0, + server_time_offset = 0, + location = "http://test.airconsole.com", + devices = new object[] { new { location = "http://test.airconsole.com" } }, + configuration + }); + target = new GameObject("Target").AddComponent(); + target.onReady += _ => { + JToken result = target.GetConfiguration(); + Assert.IsNotNull(result, "Configuration should not be null after ready"); + Assert.AreEqual("high", (string)result["graphicsQualityTier"]); + Assert.AreEqual(true, (bool)result["transparentVideoSupported"]); + Assert.AreEqual(true, (bool)result["unityVideoSupported"]); + var formats = result["supportedVideoFormats"].ToObject(); + Assert.AreEqual(new[] { "vp9", "h264", "vp8" }, formats); + testIsDone = true; + }; + target.Initialize(); + + target.SimulateReady(readyMessage); + target.Update(); + + while (!testIsDone) { + yield return null; + } + } + public class AirConsoleTestRunner : AirConsole, IMonoBehaviourTest { private int frameCount; @@ -111,6 +155,13 @@ private void Start() { base.SetSafeArea(message); } + internal void SimulateReady(JObject message) { + // Use reflection to invoke the private OnReady method + var method = typeof(AirConsole).GetMethod("OnReady", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + method.Invoke(this, new object[] { message }); + } + internal void Initialize() { Awake(); Start(); From e25334a336f1f5e65c7ec74e54bab5e529cbe1c7 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Mon, 30 Mar 2026 23:52:42 +0200 Subject: [PATCH 2/3] d B: Improve GetConfiguration() XML doc with screen-only note (ENG-74, Task 6) Clarify that GetConfiguration() is only available on the screen device, returns null on controllers, and is delivered via the ready event. --- Assets/AirConsole/scripts/Runtime/AirConsole.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index afac84f..b982beb 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -659,8 +659,9 @@ public long GetServerTime() { } /// - /// Returns the platform capability configuration. - /// Use this to branch on capabilities instead of platform or partner names. + /// Returns the platform capability configuration delivered in the ready event. + /// Use this to branch on device capabilities instead of platform or partner + /// names. Only available on the screen; controllers receive null. /// Can only be called after OnReady. /// /// @@ -669,7 +670,7 @@ public long GetServerTime() { /// transparentVideoSupported (bool) /// unityVideoSupported (bool) /// graphicsQualityTier (string) - "low", "medium", or "high" - /// Returns null if not yet received. + /// Returns null if not yet received or on controllers. /// public JToken GetConfiguration() { if (!IsAirConsoleUnityPluginReady()) { From f465f43a906c727d9dff4a0d6e182e94fe216a26 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Tue, 31 Mar 2026 00:18:47 +0200 Subject: [PATCH 3/3] t B: Add 3 Unity EditMode tests for GetConfiguration() (M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BeforeReady_ThrowsNotReadyException, AfterResetCaches_ReturnsNull, WhenReadyDataLacksConfiguration_ReturnsNull — closes review finding M3. Also adds SimulateResetCaches() reflection helper to AirConsoleTestRunner. --- .../scripts/Tests/EditMode/AirConsoleTests.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs b/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs index a80ade6..8980736 100644 --- a/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs +++ b/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs @@ -126,6 +126,97 @@ public IEnumerator GetConfiguration_AfterReady_ReturnsConfiguration() { } } + [UnityTest] + [Timeout(300)] + public IEnumerator GetConfiguration_BeforeReady_ThrowsNotReadyException() { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { + Assert.Inconclusive("This test requires an Android build target"); + } + + target = new GameObject("Target").AddComponent(); + target.Initialize(); + + // GetConfiguration() must throw before the READY message has been received. + Assert.Throws(() => target.GetConfiguration()); + + yield return null; + } + + [UnityTest] + [Timeout(300)] + public IEnumerator GetConfiguration_AfterResetCaches_ReturnsNull() { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { + Assert.Inconclusive("This test requires an Android build target"); + } + + bool testIsDone = false; + JObject configuration = JObject.FromObject(new { + supportedVideoFormats = new[] { "vp9", "h264", "vp8" }, + transparentVideoSupported = true, + unityVideoSupported = true, + graphicsQualityTier = "high" + }); + JObject readyMessage = JObject.FromObject(new { + action = "ready", + code = "test123", + device_id = 0, + server_time_offset = 0, + location = "http://test.airconsole.com", + devices = new object[] { new { location = "http://test.airconsole.com" } }, + configuration + }); + target = new GameObject("Target").AddComponent(); + target.onReady += _ => { + Assert.IsNotNull(target.GetConfiguration(), "Should have config after ready"); + // Simulate a reconnect / reload that clears caches — the field must be null + // (not stale) before the subsequent ready message arrives. + target.SimulateResetCaches(); + Assert.Throws(() => target.GetConfiguration()); + testIsDone = true; + }; + target.Initialize(); + + target.SimulateReady(readyMessage); + target.Update(); + + while (!testIsDone) { + yield return null; + } + } + + [UnityTest] + [Timeout(300)] + public IEnumerator GetConfiguration_WhenReadyDataLacksConfiguration_ReturnsNull() { + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { + Assert.Inconclusive("This test requires an Android build target"); + } + + bool testIsDone = false; + // Ready message without a "configuration" key — server may omit the field. + JObject readyMessage = JObject.FromObject(new { + action = "ready", + code = "test123", + device_id = 0, + server_time_offset = 0, + location = "http://test.airconsole.com", + devices = new object[] { new { location = "http://test.airconsole.com" } } + }); + target = new GameObject("Target").AddComponent(); + target.onReady += _ => { + JToken result = target.GetConfiguration(); + Assert.IsNull(result, "Configuration should be null when not present in ready data"); + testIsDone = true; + }; + target.Initialize(); + + target.SimulateReady(readyMessage); + target.Update(); + + while (!testIsDone) { + yield return null; + } + } + public class AirConsoleTestRunner : AirConsole, IMonoBehaviourTest { private int frameCount; @@ -162,6 +253,14 @@ internal void SimulateReady(JObject message) { method.Invoke(this, new object[] { message }); } + internal void SimulateResetCaches() { + // Use reflection to invoke the private ResetCaches method. + // Passes a no-op action because we do not need the post-clear callback in tests. + var method = typeof(AirConsole).GetMethod("ResetCaches", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + method.Invoke(this, new object[] { (System.Action)(() => { }) }); + } + internal void Initialize() { Awake(); Start();