Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Assets/AirConsole/scripts/Runtime/AirConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,28 @@ public long GetServerTime() {
return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds + _server_time_offset;
}

/// <summary>
/// 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.
/// </summary>
/// <returns>
/// 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.
/// </returns>
public JToken GetConfiguration() {
if (!IsAirConsoleUnityPluginReady()) {
throw new NotReadyException();
}

return _configuration;
}

/// <summary>
/// Request that all devices return to the AirConsole store.
/// </summary>
Expand Down Expand Up @@ -1533,6 +1555,9 @@ private void OnReady(JObject msg) {
_devices.Add(assign);
}

// parse configuration
_configuration = msg["configuration"];

_receivedReady = true;

if (onReady != null) {
Expand Down Expand Up @@ -1629,6 +1654,7 @@ private void ResetCaches(Action taskToQueueAfterClear) {
_server_time_offset = 0;
_location = null;
_translations = null;
_configuration = null;
_receivedReady = false;

// Reset safe area
Expand Down Expand Up @@ -1990,6 +2016,7 @@ internal static bool IsAndroidOrEditor {
private int _server_time_offset;
private string _location;
private Dictionary<string, string> _translations;
private JToken _configuration;
private readonly List<int> _players = new();
private readonly Queue<Action> eventQueue = new();
private bool _safeAreaWasSet;
Expand Down
150 changes: 150 additions & 0 deletions Assets/AirConsole/scripts/Tests/EditMode/AirConsoleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AirConsoleTestRunner>();
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<string[]>();
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<AirConsoleTestRunner>();
target.Initialize();

// GetConfiguration() must throw before the READY message has been received.
Assert.Throws<AirConsole.NotReadyException>(() => 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<AirConsoleTestRunner>();
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<AirConsole.NotReadyException>(() => 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<AirConsoleTestRunner>();
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;

Expand Down Expand Up @@ -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();
Expand Down
Loading