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
8 changes: 8 additions & 0 deletions Runtime/Scripts/UniTask.meta

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

93 changes: 93 additions & 0 deletions Runtime/Scripts/UniTask/YieldInstructionUniTaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#if LIVEKIT_UNITASK
using System.Threading;
using Cysharp.Threading.Tasks;

namespace LiveKit
{
/// <summary>
/// Bridges the SDK's <see cref="YieldInstruction"/> / <see cref="StreamYieldInstruction"/>
/// surface to UniTask, adding <see cref="CancellationToken"/> support. Available only when
/// the <c>com.cysharp.unitask</c> package is installed; the assembly is otherwise excluded
/// via a defineConstraint on <c>LIVEKIT_UNITASK</c>.
/// </summary>
public static class YieldInstructionUniTaskExtensions
{
/// <summary>
/// Wraps the instruction as a <see cref="UniTask"/>. The task completes when the
/// instruction's <see cref="YieldInstruction.IsDone"/> transitions to true, or
/// faults with <see cref="System.OperationCanceledException"/> if the token fires
/// first.
/// </summary>
/// <remarks>
/// Cancellation has "abandon awaiter" semantics: the underlying FFI request keeps
/// running and any result is discarded. Wire-level cancellation is not yet
/// supported. Error inspection stays on the instruction itself — the awaiter does
/// not throw on <see cref="YieldInstruction.IsError"/>, matching the existing
/// <c>yield return</c> / <c>await</c> behavior.
/// </remarks>
public static UniTask AsUniTask(this YieldInstruction instruction, CancellationToken cancellationToken = default)
{
if (instruction == null) throw new System.ArgumentNullException(nameof(instruction));
if (instruction.IsDone) return UniTask.CompletedTask;
if (cancellationToken.IsCancellationRequested) return UniTask.FromCanceled(cancellationToken);

var source = new UniTaskCompletionSource();
CancellationTokenRegistration registration = default;

if (cancellationToken.CanBeCanceled)
{
registration = cancellationToken.Register(static state =>
{
var s = (UniTaskCompletionSource)state;
s.TrySetCanceled();
}, source);
}

// YieldInstruction.RegisterContinuation fires the callback exactly once and is
// race-safe between FFI-thread completion and main-thread registration. Either
// TrySetResult or TrySetCanceled wins; the loser is a no-op.
instruction.GetAwaiter().OnCompleted(() =>
{
registration.Dispose();
source.TrySetResult();
});

return source.Task;
}

/// <summary>
/// UniTask-bridged equivalent of awaiting a <see cref="StreamYieldInstruction"/> once.
/// Call <see cref="StreamYieldInstruction.Reset"/> between chunks; each
/// <c>AsUniTask</c> call awaits the next chunk or end-of-stream.
/// </summary>
public static UniTask AsUniTask(this StreamYieldInstruction instruction, CancellationToken cancellationToken = default)
{
if (instruction == null) throw new System.ArgumentNullException(nameof(instruction));
// GetAwaiter().IsCompleted folds together IsCurrentReadDone || IsEos and is
// the only public way to check the combined state from outside the LiveKit asm.
if (instruction.GetAwaiter().IsCompleted) return UniTask.CompletedTask;
if (cancellationToken.IsCancellationRequested) return UniTask.FromCanceled(cancellationToken);

var source = new UniTaskCompletionSource();
CancellationTokenRegistration registration = default;

if (cancellationToken.CanBeCanceled)
{
registration = cancellationToken.Register(static state =>
{
var s = (UniTaskCompletionSource)state;
s.TrySetCanceled();
}, source);
}

instruction.GetAwaiter().OnCompleted(() =>
{
registration.Dispose();
source.TrySetResult();
});

return source.Task;
}
}
}
#endif
11 changes: 11 additions & 0 deletions Runtime/Scripts/UniTask/YieldInstructionUniTaskExtensions.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 Runtime/Scripts/UniTask/livekit.unity.Runtime.UniTask.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "LiveKit.UniTask",
"rootNamespace": "LiveKit.UniTaskExtensions",
"references": [
"LiveKit",
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [
"LIVEKIT_UNITASK"
],
"versionDefines": [
{
"name": "com.cysharp.unitask",
"expression": "2.0.0",
"define": "LIVEKIT_UNITASK"
}
],
"noEngineReferences": false
}

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

52 changes: 32 additions & 20 deletions Samples~/Meet/Assets/Runtime/MeetManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using LiveKit;
Expand Down Expand Up @@ -97,9 +98,14 @@ private void OnDestroy()

#region UI Callbacks

// Action-compatible button handlers; the async work fires-and-forgets via Forget()
// so exceptions are surfaced through UniTask's tracker instead of swallowed by
// async void. Cancellation is tied to the MonoBehaviour lifetime via
// GetCancellationTokenOnDestroy so scene change / app quit aborts in-flight FFI awaits.

private void OnStartCall()
{
StartCoroutine(ConnectToRoom());
ConnectToRoom(this.GetCancellationTokenOnDestroy()).Forget();
}

private void OnEndCall()
Expand All @@ -116,7 +122,7 @@ private void OnEndCall()
private void OnToggleCamera()
{
if (!_cameraActive)
StartCoroutine(PublishLocalCamera());
PublishLocalCamera(this.GetCancellationTokenOnDestroy()).Forget();
else
UnpublishLocalCamera();
}
Expand All @@ -125,7 +131,7 @@ private void OnToggleMicrophone()
{
if (!_microphoneActive)
{
StartCoroutine(PublishLocalMicrophone());
PublishLocalMicrophone(this.GetCancellationTokenOnDestroy()).Forget();
buttonBar.SetMicrophoneOn(true);
}
else
Expand All @@ -147,17 +153,17 @@ private void OnPublishData()

#region Connection

private IEnumerator ConnectToRoom()
private async UniTask ConnectToRoom(CancellationToken cancellationToken)
{
if (_room != null) yield break;
if (_room != null) return;

var fetch = _tokenSourceComponent.FetchConnectionDetails(new TokenSourceFetchOptions());
yield return fetch;
await fetch.AsUniTask(cancellationToken);

if (fetch.IsError)
{
Debug.LogError($"Failed to fetch connection details: {fetch.Exception?.Message}");
yield break;
return;
}

var details = fetch.Result;
Expand All @@ -172,13 +178,13 @@ private IEnumerator ConnectToRoom()
_room.DataReceived += OnDataReceived;

var connect = _room.Connect(details.ServerUrl, details.ParticipantToken, new RoomOptions());
yield return connect;
await connect.AsUniTask(cancellationToken);

if (connect.IsError)
{
Debug.LogError("LiveKit connection failed");
_room = null;
yield break;
return;
}

Debug.Log($"Connected to {_room.Name}");
Expand Down Expand Up @@ -390,14 +396,19 @@ private void OnTrackUnmuted(TrackPublication publication, Participant participan

#region Local Camera

private IEnumerator PublishLocalCamera()
private async UniTask PublishLocalCamera(CancellationToken cancellationToken)
{
if (_cameraActive) yield break;
if (_cameraActive) return;

if (_webCamTexture == null)
yield return CameraDeviceProvider.Open(frameRate, t => _webCamTexture = t);
{
// CameraDeviceProvider.Open is still a plain IEnumerator (it touches Unity's
// WebCamTexture lifecycle), so bridge it through UniTask's IEnumerator adapter.
await CameraDeviceProvider.Open(frameRate, t => _webCamTexture = t)
.ToUniTask(cancellationToken: cancellationToken);
}

if (_webCamTexture == null) yield break;
if (_webCamTexture == null) return;

EnsureParticipantTile(_localId);
var tile = _participantTiles[_localId];
Expand All @@ -414,9 +425,9 @@ private IEnumerator PublishLocalCamera()
};

var publish = _room.LocalParticipant.PublishTrack(_localVideoTrack, options);
yield return publish;
await publish.AsUniTask(cancellationToken);

if (publish.IsError) yield break;
if (publish.IsError) return;

source.TextureReceived += tex =>
{
Expand All @@ -427,6 +438,7 @@ private IEnumerator PublishLocalCamera()
_cameraActive = true;
_localRtcVideoSource = source;
source.Start();
// Long-running per-frame pump — keep on the coroutine driver, not awaitable.
StartCoroutine(source.Update());

buttonBar.SetCameraOn(true);
Expand All @@ -449,9 +461,9 @@ private void UnpublishLocalCamera()

#region Local Microphone

private IEnumerator PublishLocalMicrophone()
private async UniTask PublishLocalMicrophone(CancellationToken cancellationToken)
{
if (_audioObjects.ContainsKey(LocalAudioTrackName)) yield break;
if (_audioObjects.ContainsKey(LocalAudioTrackName)) return;

Microphone.Start(null, true, 10, 44100);

Expand All @@ -469,9 +481,9 @@ private IEnumerator PublishLocalMicrophone()
};

var publish = _room.LocalParticipant.PublishTrack(_localAudioTrack, options);
yield return publish;
await publish.AsUniTask(cancellationToken);

if (publish.IsError) yield break;
if (publish.IsError) return;

_microphoneActive = true;
_audioObjects[LocalAudioTrackName] = audioObject;
Expand Down
1 change: 1 addition & 0 deletions Samples~/Meet/Packages/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"dependencies": {
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"com.unity.collab-proxy": "2.2.0",
"com.unity.feature.2d": "2.0.0",
"com.unity.ide.rider": "3.0.27",
Expand Down
7 changes: 7 additions & 0 deletions Samples~/Meet/Packages/packages-lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"dependencies": {
"com.cysharp.unitask": {
"version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "e5acc106ee196bc5a32fb14cdf2987b0f96d11e0"
},
"com.unity.2d.animation": {
"version": "9.1.0",
"depth": 1,
Expand Down
8 changes: 8 additions & 0 deletions Tests/PlayMode/UniTask.meta

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

31 changes: 31 additions & 0 deletions Tests/PlayMode/UniTask/LiveKit.PlayModeTests.UniTask.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "PlayModeTests.UniTask",
"rootNamespace": "LiveKit.PlayModeTests.UniTask",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"LiveKit",
"LiveKit.UniTask",
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS",
"LIVEKIT_UNITASK"
],
"versionDefines": [
{
"name": "com.cysharp.unitask",
"expression": "2.0.0",
"define": "LIVEKIT_UNITASK"
}
],
"noEngineReferences": false
}

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

Loading
Loading