diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs
index bda49b8..b982beb 100644
--- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs
+++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs
@@ -658,6 +658,28 @@ public long GetServerTime() {
return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds + _server_time_offset;
}
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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 or on controllers.
+ ///
+ public JToken GetConfiguration() {
+ if (!IsAirConsoleUnityPluginReady()) {
+ throw new NotReadyException();
+ }
+
+ return _configuration;
+ }
+
///
/// Request that all devices return to the AirConsole store.
///
@@ -1533,6 +1555,9 @@ private void OnReady(JObject msg) {
_devices.Add(assign);
}
+ // parse configuration
+ _configuration = msg["configuration"];
+
_receivedReady = true;
if (onReady != null) {
@@ -1629,6 +1654,7 @@ private void ResetCaches(Action taskToQueueAfterClear) {
_server_time_offset = 0;
_location = null;
_translations = null;
+ _configuration = null;
_receivedReady = false;
// Reset safe area
@@ -1990,6 +2016,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..8980736 100644
--- a/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs
+++ b/Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs
@@ -82,6 +82,141 @@ 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;
+ }
+ }
+
+ [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;
@@ -111,6 +246,21 @@ 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 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();