Skip to content

Stage 2: Add optional UniTask surface behind LIVEKIT_UNITASK#290

Open
MaxHeimbrock wants to merge 1 commit into
max/yield-instruction-getawaiterfrom
max/yield-instruction-unitask-extension
Open

Stage 2: Add optional UniTask surface behind LIVEKIT_UNITASK#290
MaxHeimbrock wants to merge 1 commit into
max/yield-instruction-getawaiterfrom
max/yield-instruction-unitask-extension

Conversation

@MaxHeimbrock
Copy link
Copy Markdown
Contributor

Summary

  • New LiveKit.UniTask asmdef hosts an AsUniTask extension on YieldInstruction and StreamYieldInstruction.
  • Asmdef compiles only when com.cysharp.unitask is installed — versionDefine auto-activates LIVEKIT_UNITASK. When UniTask is absent, the extension simply does not exist; no compile error, no runtime cost.
  • AsUniTask(CancellationToken ct = default) wraps the Stage 1 awaiter in a UniTaskCompletionSource and adds cancellation with abandon-awaiter semantics — a cancel faults the UniTask with OperationCanceledException but the underlying FFI request is not aborted. Wire-level cancellation is deferred.
  • GetResult stays non-throwing for IsError parity with yield return / await (callers still inspect IsError). Throwing variants can be layered on later.

Sample migration

Samples~/Meet/Assets/Runtime/MeetManager.cs is migrated end-to-end to demonstrate the path: ConnectToRoom, PublishLocalCamera, PublishLocalMicrophone all switch to async UniTask with cancellation bound to this.GetCancellationTokenOnDestroy(). Event handlers stay Action-compatible (void) and fire-and-forget via .Forget(). Long-running per-frame pumps (source.Update(), stream.Update()) stay on StartCoroutine since they are not request/response.

Testing

  • Unit tests (Tests/PlayMode/UniTask/RoomUniTaskTests.cs) — gated by the same LIVEKIT_UNITASK define:
    • AsUniTask_CompletesOnIsDone — synthetic instruction, verifies UniTask resumes when IsDone is set and IsError is observable on resume.
    • AsUniTask_Cancellation_ThrowsOperationCanceled — synthetic instruction, verifies cancellation throws OperationCanceledException and the underlying instruction is untouched.
  • An additional E2E test against the real FFI Connect path was tried and removed: FFI error logs arrive asynchronously and their delivery window races UniTask's synchronous resume, so LogAssert tracking was brittle across test order. The unit tests cover the extension's logic; end-to-end coverage is now the migrated Meet sample.

Targeting

Base branch is max/yield-instruction-getawaiter (Stage 1 PR #289), so this diff shows only the Stage 2 surface area.

Test plan

  • Scripts~/run_unity.sh build macos — clean compile with UniTask restored via UPM git URL.
  • All 4 PlayMode tests pass (Connect_FailsWithInvalidUrl|AsUniTask filter): 4 passed, 0 failed.
  • Reviewer: run livekit-server --dev, then open Samples~/Meet and verify Start Call / Camera / Mic flows all work end-to-end on UniTask.
  • Reviewer: confirm that omitting UniTask from the manifest (e.g. for Samples~/Basic) does not produce a compile error — LiveKit.UniTask asmdef should be cleanly skipped.

🤖 Generated with Claude Code

Stage 2 of the UniTask migration. The new LiveKit.UniTask asmdef hosts
an AsUniTask extension on YieldInstruction and StreamYieldInstruction;
the asmdef compiles only when com.cysharp.unitask is installed (the
versionDefine auto-activates LIVEKIT_UNITASK). When UniTask is absent,
the extension simply does not exist — no compile error, no runtime cost,
no impact on Stage 1's awaiter.

AsUniTask wraps the existing one-shot completion path in a UniTaskCompletionSource
and adds CancellationToken support with "abandon awaiter" semantics: a cancel
faults the UniTask with OperationCanceledException, but the underlying FFI
request is not aborted. GetResult stays non-throwing for IsError parity with
yield return / await; throwing variants can be layered on later.

Includes a UniTask migration of Samples~/Meet to demonstrate the new path
end-to-end (Connect / PublishLocalCamera / PublishLocalMicrophone all switch
to async UniTask with cancellation tied to GetCancellationTokenOnDestroy).
Long-running per-frame pumps stay on StartCoroutine since they aren't
request/response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant