Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ downloads~
# Ignore temporaries from GameCI
**/[Aa]rtifacts/
**/[Cc]odeCoverage/

# Here we keep private credentials
LiveKitCredentialsExtension.cs
LiveKitCredentialsExtension.cs.meta
2 changes: 1 addition & 1 deletion Runtime/Scripts/RtcVideoSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public enum VideoStreamSource
private bool _muted = false;
public override bool Muted => _muted;

internal RtcVideoSource(VideoStreamSource sourceType, VideoBufferType bufferType)
protected RtcVideoSource(VideoStreamSource sourceType, VideoBufferType bufferType)
{
_sourceType = sourceType;
_bufferType = bufferType;
Expand Down
831 changes: 831 additions & 0 deletions Tests/PlayMode/LatencyTests.cs

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Tests/PlayMode/LatencyTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Tests/PlayMode/LiveKit.PlayModeTests.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
"nunit.framework.dll",
"Google.Protobuf.dll"
],
"autoReferenced": false,
"defineConstraints": [
Expand Down
89 changes: 89 additions & 0 deletions Tests/PlayMode/Utils/AudioPulseDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using UnityEngine;

namespace LiveKit.PlayModeTests.Utils
{
/// <summary>
/// MonoBehaviour that identifies audio pulses by frequency using the Goertzel algorithm.
/// Attach to the same GameObject as an AudioSource after AudioStream has added its
/// AudioProbe, so this filter runs second and sees the filled audio data.
/// </summary>
public class AudioPulseDetector : MonoBehaviour
{
public int TotalPulses;
public double BaseFrequency;
public double FrequencyStep;
public double MagnitudeThreshold;

/// <summary>
/// Fired on the audio thread when a pulse is detected.
/// Parameters: (pulseIndex, magnitude).
/// </summary>
public event Action<int, double> PulseReceived;

private int _sampleRate;

void OnEnable()
{
_sampleRate = AudioSettings.outputSampleRate;
AudioSettings.OnAudioConfigurationChanged += OnAudioConfigChanged;
}

void OnDisable()
{
AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigChanged;
}

void OnAudioConfigChanged(bool deviceWasChanged)
{
_sampleRate = AudioSettings.outputSampleRate;
}

void OnAudioFilterRead(float[] data, int channels)
{
int sampleRate = _sampleRate;
int samples = data.Length / channels;
if (samples == 0) return;

int bestPulse = -1;
double bestMag = 0;

for (int p = 0; p < TotalPulses; p++)
{
double freq = BaseFrequency + p * FrequencyStep;
double mag = Goertzel(data, channels, samples, sampleRate, freq);
if (mag > bestMag)
{
bestMag = mag;
bestPulse = p;
}
}

// Debug.Log($"bestPulse: {bestPulse} | bestMag: {bestMag}");
if (bestPulse >= 0 && bestMag > MagnitudeThreshold)
PulseReceived?.Invoke(bestPulse, bestMag);
}

/// <summary>
/// Goertzel algorithm — computes the magnitude of a single frequency bin.
/// O(N) per frequency, much cheaper than a full FFT.
/// </summary>
static double Goertzel(float[] data, int channels, int N, int sampleRate, double freq)
{
double k = 0.5 + (double)N * freq / sampleRate;
double w = 2.0 * Math.PI * k / N;
double coeff = 2.0 * Math.Cos(w);
double s0 = 0, s1 = 0, s2 = 0;

for (int i = 0; i < N; i++)
{
s0 = data[i * channels] + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}

double power = s1 * s1 + s2 * s2 - coeff * s1 * s2;
return Math.Sqrt(Math.Abs(power)) / N;
}
}
}
11 changes: 11 additions & 0 deletions Tests/PlayMode/Utils/AudioPulseDetector.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Tests/PlayMode/Utils/LiveKitCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

public struct LiveKitCredentials
{
public string ServerUrl;
public string ApiKey;
public string ApiSecret;

public static LiveKitCredentials CreateLocalDevCredentials()
{
return new LiveKitCredentials(){
ServerUrl = "ws://localhost:7880",
ApiKey = "devkey",
ApiSecret = "secret"
};
}

public static LiveKitCredentials CreateFromEnv()
{
return new LiveKitCredentials(){
ServerUrl = ReadEnv("LK_TEST_URL", "ws://localhost:7880"),
ApiKey = ReadEnv("LK_TEST_API_KEY", "devkey"),
ApiSecret = ReadEnv("LK_TEST_API_SECRET", "secret")
};
}

private static string ReadEnv(string key, string defaultValue)
{
var value = Environment.GetEnvironmentVariable(key);
if (string.IsNullOrEmpty(value))
return defaultValue;
return value.Trim();
}
}
11 changes: 11 additions & 0 deletions Tests/PlayMode/Utils/LiveKitCredentials.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions Tests/PlayMode/Utils/TestAudioSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace LiveKit.PlayModeTests.Utils
{
/// <summary>
/// A programmatic audio source for testing. Allows pushing audio frames
/// directly without requiring a Unity AudioSource or microphone.
/// </summary>
public class TestAudioSource : RtcAudioSource
{
public override event Action<float[], int, int> AudioRead;

public TestAudioSource(int channels = 1)
: base(channels, RtcAudioSourceType.AudioSourceCustom) { }

/// <summary>
/// Push an audio frame into the FFI capture pipeline.
/// Must call Start() first so the base class is subscribed to AudioRead.
/// </summary>
public void PushFrame(float[] data, int channels, int sampleRate)
{
AudioRead?.Invoke(data, channels, sampleRate);
}
}
}
11 changes: 11 additions & 0 deletions Tests/PlayMode/Utils/TestAudioSource.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions Tests/PlayMode/Utils/TestCoroutineRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections;
using UnityEngine;

namespace LiveKit.PlayModeTests.Utils
{
/// <summary>
/// Helper for running coroutines from non-MonoBehaviour test code.
/// Creates a temporary GameObject with a MonoBehaviour to host coroutines.
/// </summary>
public class TestCoroutineRunner : MonoBehaviour
{
private static TestCoroutineRunner _instance;

private static TestCoroutineRunner Instance
{
get
{
if (_instance == null)
{
var go = new GameObject("TestCoroutineRunner");
_instance = go.AddComponent<TestCoroutineRunner>();
}
return _instance;
}
}

public static Coroutine Start(IEnumerator routine)
{
return Instance.StartCoroutine(routine);
}

public static void Stop(Coroutine coroutine)
{
if (_instance != null && coroutine != null)
_instance.StopCoroutine(coroutine);
}

public static void Cleanup()
{
if (_instance != null)
{
Destroy(_instance.gameObject);
_instance = null;
}
}
}
}
11 changes: 11 additions & 0 deletions Tests/PlayMode/Utils/TestCoroutineRunner.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 14 additions & 15 deletions Tests/PlayMode/Utils/TestRoomContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class TestRoomContext : IDisposable
private List<ConnectionOptions> _connectionOptions;
private bool _disposed = false;

private static LiveKitCredentials _credentials;

public TestRoomContext() : this(ConnectionOptions.Default) {}
public TestRoomContext(ConnectionOptions options) : this(new[] { options }) {}

Expand All @@ -26,6 +28,15 @@ public TestRoomContext(IEnumerable<ConnectionOptions> options)
return options;
});
_connectionOptions = withDefaults.ToList();

_credentials = LiveKitCredentials.CreateFromEnv();
// _credentials = LiveKitCredentials.CreateLocalDevCredentials(); // Use for explicit dev server usage
// _credentials = LiveKitCredentialsExtension.CreateSandboxCredentials(); // Use for explicit sandbox server usage
}

public String GetServerUrl()
{
return _credentials.ServerUrl;
}

public struct ConnectionOptions
Expand Down Expand Up @@ -57,7 +68,7 @@ public IEnumerator ConnectAll()
var token = CreateToken(options);
var room = new Room();
var roomOptions = new RoomOptions();
var connect = room.Connect(options.ServerUrl ?? _serverUrl, token, roomOptions);
var connect = room.Connect(options.ServerUrl ?? _credentials.ServerUrl, token, roomOptions);
yield return connect;

if (connect.IsError)
Expand All @@ -81,7 +92,7 @@ private string CreateToken(ConnectionOptions options)
{
var claims = new AccessToken.Claims
{
iss = _apiKey,
iss = _credentials.ApiKey,
sub = options.Identity,
name = options.DisplayName,
video = new AccessToken.VideoGrants
Expand All @@ -96,7 +107,7 @@ private string CreateToken(ConnectionOptions options)
},
metadata = options.Metadata
};
return AccessToken.Encode(claims, _apiSecret);
return AccessToken.Encode(claims, _credentials.ApiSecret);
}

public void Dispose()
Expand All @@ -111,17 +122,5 @@ protected virtual void Dispose(bool disposing)
if (disposing) DisconnectAll();
_disposed = true;
}

private static string _serverUrl => ReadEnv("LK_TEST_URL", "ws://localhost:7880");
private static string _apiKey => ReadEnv("LK_TEST_API_KEY", "devkey");
private static string _apiSecret => ReadEnv("LK_TEST_API_SECRET", "secret");

private static string ReadEnv(string key, string defaultValue)
{
var value = Environment.GetEnvironmentVariable(key);
if (string.IsNullOrEmpty(value))
return defaultValue;
return value.Trim();
}
}
}
Loading
Loading