Use this SDK to add realtime video, audio and data features to your Unity app. By connecting to LiveKit Cloud or a self-hosted server, you can quickly build applications such as multi-modal AI like voice AI agents for NPCs, live streaming, or video calls with just a few lines of code.
We officially support Unity 2022 onwards, we test on Unity 2022.3.62 and Unity 6000.3.10.
- Windows
- MacOS
- Linux
- iOS
- Android
- WebGL
We plan to support all Unity platforms with this SDK. WebGL is currently supported with client-sdk-unity-web.
Before cloning the repo, make sure that Git LFS is installed and setup. You can either clone the repo and import from the local Unity package files:
git clone https://github.com/livekit/client-sdk-unity.git
cd client-sdk-unity
Or you can import the Git url https://github.com/livekit/client-sdk-unity.git from the package manager.
If you want to use tagged release versions, use https://github.com/livekit/client-sdk-unity.git#vX.X.X, for example with #v1.3.5.
The package is also hosted in the OpenUPM package registry. Here is the guide on how to use the OpenUPM registry to import the package: https://openupm.com/packages/io.livekit.livekit-sdk/#modal-manualinstallation
For local development, initialize the Git submodule containing the Rust code for the LiveKit plugin libraries.
There is a helper script to build the libraries locally and exchange the downloaded libraries with the local build artifacts in the correct Runtime/Plugins folder.
Currently, the build script supports the following arguments:
- macos
- android
- ios
In the following options:
- debug (default)
- release
So a build command is for example:
./Scripts~/build_ffi_locally.sh macos release
Look at the Unity-SDK.code-workspace setup for VSCode. This will use the Meet Sample as the Unity project and the Unity SDK package as two roots in a multi-root workspace and the Meet.sln as the dotnet.defaultSolution, enabling Rust and C# IDE support.
For C# debugging, there is a simple attach launch option called C# Unity, for example in the Meet/.vscode/launch.json.
For Rust / C++ debugging on MacOS, you need to install the CodeLLDB extension. The debug attach is defined in .vscode/launch.json.
- Build the livekit-ffi lib locally in debug mode with
./Scripts~/build_ffi_locally.sh macos debug - Start the Unity Editor
- Attach to the Unity Editor process (either auto or manual process picker)
- Start the Scene in Editor
Add dependent frameworks to your Unity project
select Unity-iPhone -> TARGETS -> UnityFramework -> General -> Frameworks and Libraries -> +
add the following frameworks:
OpenGLES.framework MetalKit.framework GLKit.framework MetalKit.framework VideoToolBox.framework Network.framework
add other linker flags to UnityFramework:
-ObjC
Since libiPhone-lib.a has built-in old versions of celt and libvpx (This will cause the opus and vp8/vp9 codecs to not be called correctly and cause a crash.), you need to ensure that liblivekit_ffi.a is linked before libiPhone-lib.a.
The package now applies an iOS post-build fix that rewrites the exported Xcode project so libiPhone-lib.a is moved after liblivekit_ffi.a in UnityFramework -> Frameworks and Libraries.
It also strips the old CELT object cluster from the exported Libraries/libiPhone-lib.a so Xcode cannot resolve those codec symbols from Unity's archive.
If your project disables package editor scripts or uses a custom Xcode export pipeline that overwrites project.pbxproj after LiveKit runs, you may still need to adjust the order manually by removing and re-adding libiPhone-lib.a.
The repo contains these sample projects:
Most of the following functionalities and code snippets can be found in the samples in a similar form to try out.
You need a token to join a LiveKit room as a participant. Read more about tokens here: https://docs.livekit.io/frontends/reference/tokens-grants/
To help getting started with tokens, use TokenSourceComponent.cs with a TokenSourceComponentConfig ScriptableObject (see https://docs.livekit.io/frontends/build/authentication/#tokensource). Create a config asset via Right Click > Create > LiveKit > TokenSourceComponentConfig and select one of three token source types:
Use this to pass a pregenerated server URL and token. Generate tokens via the LiveKit CLI or from your LiveKit Cloud project's API key page.
For development and testing. Follow the sandbox token server guide to enable your project's sandbox and get the sandbox ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults.
For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Sandbox. See the endpoint token generation guide.
Add a TokenSourceComponent to a GameObject, assign your TokenSourceComponentConfig asset, then fetch connection details before connecting:
FetchConnectionDetails returns a TaskYieldInstruction<ConnectionDetails>, so you can yield return it from a coroutine, await it, or bridge it with .AsUniTask(). Inspect the result on the instruction itself — IsError / Exception / Result — rather than on a Task:
var fetch = _tokenSourceComponent.FetchConnectionDetails();
yield return fetch;
if (fetch.IsError)
{
Debug.LogError($"Failed to fetch connection details: {fetch.Exception?.Message}");
yield break;
}
var details = fetch.Result;
_room = new Room();
var connect = _room.Connect(details.ServerUrl, details.ParticipantToken, new RoomOptions());Per-call overrides (e.g. dynamic room or participant names) can be passed via TokenSourceFetchOptions; any field set there wins over the asset, and unset fields fall back to the config:
var fetch = _tokenSourceComponent.FetchConnectionDetails(new TokenSourceFetchOptions
{
RoomName = "lobby-" + System.Guid.NewGuid(),
ParticipantName = playerName,
});To skip the ScriptableObject entirely, instantiate a token source directly. Each returns the same TaskYieldInstruction<ConnectionDetails> from FetchConnectionDetails, so it can be yielded, awaited, or .AsUniTask()-bridged just like the component:
// Fixed sources take no per-call options:
ITokenSourceFixed source = new TokenSourceLiteral("wss://your.livekit.host", "<join-token>");
// or: new TokenSourceCustom(async () => await MyAuthFlow());
var fetch = source.FetchConnectionDetails();
yield return fetch;
var details = fetch.Result;
// Configurable sources accept TokenSourceFetchOptions per call:
ITokenSourceConfigurable configurable = new TokenSourceSandbox("<sandbox-id>");
// or: new TokenSourceEndpoint("https://your.token-server/api/token", headers);
var configurableFetch = configurable.FetchConnectionDetails(new TokenSourceFetchOptions { RoomName = "lobby" });
yield return configurableFetch;IEnumerator ConnectToRoom()
{
var serverUrl = "< your server url >";
var token = "< your token >";
var room = new Room();
var connect = room.Connect(serverUrl, token, new RoomOptions());
yield return connect;
}IEnumerator PublishCamera(Room room)
{
// Option 1: publish a WebCamera
// var source = new TextureVideoSource(webCamTexture);
// Option 2: publish a screen share
// var source = new ScreenVideoSource();
// Option 3: publishing a Unity Camera
var source = new CameraVideoSource(Camera.main);
var track = LocalVideoTrack.CreateVideoTrack("my-video-track", source, room);
var videoCoding = new VideoEncoding
{
MaxBitrate = 512000,
MaxFramerate = frameRate
};
var options = new TrackPublishOptions
{
VideoCodec = VideoCodec.Vp8,
VideoEncoding = videoCoding,
Simulcast = true,
Source = TrackSource.SourceCamera
};
var publish = room.LocalParticipant.PublishTrack(track, options);
yield return publish;
if (!publish.IsError)
{
Debug.Log("Track published!");
}
source.Start();
StartCoroutine(source.Update());
}void OnTrackSubscribed(IRemoteTrack track, RemoteTrackPublication publication, RemoteParticipant participant)
{
if (track is RemoteVideoTrack videoTrack)
{
var rawImage = GetComponent<RawImage>();
var stream = new VideoStream(videoTrack);
stream.TextureReceived += (tex) =>
{
rawImage.texture = tex;
};
StartCoroutine(stream.Update());
}
}There are two options to handle audio input and output, via "Unity Audio" or via "Platform Audio".
Using Unity Audio, the Unity Microphone and AudioSource APIs are used to pipe the audio frames. Use Unity Audio if your game needs to:
- read the audio frames, e.g. for lip syncing
- manipulate the audio frames
On mobile platforms, the WebRTC audio is handled within the audio session owned by Unity, which reduces complexity.
IEnumerator PublishLocalMicrophoneUnity(Room room)
{
Debug.Log("Publishing microphone using Unity Audio");
GameObject microphoneObject = new GameObject("my-audio-source");
var rtcSource = new MicrophoneSource(Microphone.devices[0], microphoneObject);
var track = LocalAudioTrack.CreateAudioTrack("my-audio-track", rtcSource, room);
var options = new TrackPublishOptions();
options.AudioEncoding = new AudioEncoding();
options.AudioEncoding.MaxBitrate = 64000;
options.Source = TrackSource.SourceMicrophone;
var publish = room.LocalParticipant.PublishTrack(track, options);
yield return publish;
if (!publish.IsError)
{
Debug.Log("Track published!");
}
rtcSource.Start();
}void TrackSubscribed(IRemoteTrack track, RemoteTrackPublication publication, RemoteParticipant participant)
{
if (track is RemoteAudioTrack audioTrack)
{
GameObject audioOutputObject = new GameObject(audioTrack.Sid);
var source = audioOutputObject.AddComponent<AudioSource>();
var stream = new AudioStream(audioTrack, source);
}
}With Platform Audio, the audio input and output are managed by the native ADM of WebRTC. This unlocks echo cancellation, noise suppression, auto gain control and hardware processing if available.
There are some known issues with Platform Audio, that we are working on resolving:
- On iOS, disposing of Platform Audio object stops Unity audio output
- On iOS and Unity 6, backgrounding the app breaks Platform Audio
- On MacOS with bluetooth headset, unmuting can break audio output
Make sure to initialize Platform Audio before connecting to a call.
void InitializePlatformAudio()
{
try
{
var platformAudio = new PlatformAudio();
Debug.Log($"PlatformAudio initialized: {platformAudio.RecordingDeviceCount} mics, " +
$"{platformAudio.PlayoutDeviceCount} speakers");
var (recording, playout) = platformAudio.GetDevices();
Debug.Log("Recording devices:");
foreach (var device in recording)
Debug.Log($" [{device.Index}] {device.Name}");
Debug.Log("Playout devices:");
foreach (var device in playout)
Debug.Log($" [{device.Index}] {device.Name}");
if (platformAudio.RecordingDeviceCount > 0)
platformAudio.SetRecordingDevice(0);
if (platformAudio.PlayoutDeviceCount > 0)
platformAudio.SetPlayoutDevice(0);
Debug.Log($"PlatformAudio ready. AEC={echoCancellation}, NS={noiseSuppression}, AGC={autoGainControl}, HW={preferHardwareProcessing}");
}
catch (System.Exception e)
{
Debug.LogError($"Failed to initialize PlatformAudio, falling back to Unity audio: {e.Message}");
usePlatformAudio = false;
platformAudio = null;
}
}IEnumerator PublishLocalMicrophonePlatform(PlatformAudio platformAudio, Room room)
{
if (platformAudio != null)
{
yield return platformAudio.StartRecording();
}
var audioOptions = new AudioProcessingOptions
{
EchoCancellation = echoCancellation,
NoiseSuppression = noiseSuppression,
AutoGainControl = autoGainControl,
PreferHardware = preferHardwareProcessing
};
var platformAudioSource = new PlatformAudioSource(platformAudio, audioOptions);
var localAudioTrack = LocalAudioTrack.CreateAudioTrack(LocalAudioTrackName, platformAudioSource, room);
var options = new TrackPublishOptions
{
AudioEncoding = new AudioEncoding { MaxBitrate = 64000 },
Source = TrackSource.SourceMicrophone
};
var publish = room.LocalParticipant.PublishTrack(localAudioTrack, options);
yield return publish;
if (publish.IsError)
{
Debug.LogError("Failed to publish microphone track");
platformAudioSource?.Dispose();
yield break;
}
Debug.Log("Microphone published via PlatformAudio (AEC enabled)");
}Using Platform Audio, for audio output of subscribed remote audio tracks you don't need any Unity handling.
Perform your own predefined method calls from one participant to another.
This feature is especially powerful when used with Agents, for instance to forward LLM function calls to your client application.
The following is a brief overview but more detail is available in the documentation.
The participant who implements the method and will receive its calls must first register support. Your method handler will be an async callback that receives an RpcInvocationData object:
void OnRoomConnected(Room room)
{
room.LocalParticipant.RegisterRpcMethod("greet", HandleGreeting);
}
async Task<string> HandleGreeting(RpcInvocationData data)
{
Debug.Log($"Received greeting from {data.CallerIdentity}: {data.Payload}");
return $"Hello, {data.CallerIdentity}!";
}In addition to the payload, RpcInvocationData also contains responseTimeout, which informs you the maximum time available to return a response. If you are unable to respond in time, the call will result in an error on the caller's side.
The caller may initiate an RPC call using coroutines:
IEnumerator PerformRpcCoroutine(Room room)
{
var rpcCall = room.LocalParticipant.PerformRpc(new PerformRpcParams
{
DestinationIdentity = "recipient-identity",
Method = "greet",
Payload = "Hello from RPC!"
});
yield return rpcCall;
if (rpcCall.IsError)
{
Debug.Log($"RPC call failed: {rpcCall.Error}");
}
else
{
Debug.Log($"RPC response: {rpcCall.Payload}");
}
}You may find it useful to adjust the ResponseTimeout parameter, which indicates the amount of time you will wait for a response. We recommend keeping this value as low as possible while still satisfying the constraints of your application.
LiveKit is a dynamic realtime environment and RPC calls can fail for various reasons.
You may throw errors of the type RpcError with a string message in an RPC method handler and they will be received on the caller's side with the message intact. Other errors will not be transmitted and will instead arrive to the caller as 1500 ("Application Error"). Other built-in errors are detailed in the docs.
Use text streams to send any amount of text between participants.
IEnumerator SendText()
{
var text = "Lorem ipsum dolor sit amet...";
var sendTextInstruction = room.LocalParticipant.SendText("Hello from Unity", "Chat");
yield return sendTextInstruction;
}IEnumerator StreamText(Room room)
{
var streamWriter = room.LocalParticipant.StreamText("Chat");
yield return streamWriter;
string[] textChunks = {"Lorem ", "ipsum ", "dolor ", "sit ", "amet..."};
foreach (var textChunk in textChunks)
{
Debug.Log($"Sending {textChunk}");
var instruction = streamWriter.Writer.Write(textChunk);
yield return instruction;
}
yield return streamWriter.Writer.Close();
}void OnRoomConnected(Room room)
{
room.RegisterTextStreamHandler("Chat", (reader, identity) => StartCoroutine(OnTextStream(reader, identity)));
}
IEnumerator OnTextStream(TextStreamReader reader, string identity)
{
// Option 1: Process the stream incrementally
var readIncremental = reader.ReadIncremental();
while (true)
{
yield return readIncremental;
if (readIncremental.IsEos)
break;
Debug.Log(readIncremental.Text);
readIncremental.Reset();
}
// Option 2: Get the entire text after the stream completes
var readAllCall = reader.ReadAll();
yield return readAllCall;
Debug.Log($"Received text: {readAllCall.Text}");
}Use byte streams to send files, images, or any other kind of data between participants.
IEnumerator SendFile()
{
var filePath = "path/to/file.jpg";
Debug.Log($"Sending file {filePath}");
var sendFileCall = room.LocalParticipant.SendFile(filePath, "my-topic");
yield return sendFileCall;
if (sendFileCall.IsError)
{
Debug.LogError("File not found");
}
}IEnumerator StreamBytes()
{
var streamBytesCall = room.LocalParticipant.StreamBytes("my-topic");
yield return streamBytesCall;
var writer = streamBytesCall.Writer;
Debug.Log($"Opened byte stream with ID: {writer.Info.Id}");
var dataChunks = new[]
{
new byte[] { 0x00, 0x01 },
new byte[] { 0x02, 0x03 }
};
foreach (var chunk in dataChunks)
{
yield return writer.Write(chunk);
}
yield return writer.Close();
}void OnRoomConnected(Room room)
{
room.RegisterByteStreamHandler("my-topic", (reader, identity) => StartCoroutine(HandleByteStream(reader, identity)));
}
IEnumerator HandleByteStream(ByteStreamReader reader, string participantIdentity)
{
var info = reader.Info;
// Option 1: Process the stream incrementally
var readIncremental = reader.ReadIncremental();
while (true)
{
yield return readIncremental;
if (readIncremental.IsEos) break;
foreach (var dataByte in readIncremental.Bytes)
Debug.Log($"Received {dataByte}");
readIncremental.Reset();
}
// Option 2: Get the entire file after the stream completes
var readAllCall = reader.ReadAll();
yield return readAllCall;
var data = readAllCall.Bytes;
foreach (var dataByte in data)
Debug.Log($"Received {dataByte}");
// Option 3: Write the stream to a local file on disk as it arrives
var writeToFileCall = reader.WriteToFile();
yield return writeToFileCall;
var path = writeToFileCall.FilePath;
Debug.Log($"Wrote to file: {path}");
Debug.Log($@"
Byte stream received from {participantIdentity}
Topic: {info.Topic}
Timestamp: {info.Timestamp}
ID: {info.Id}
Size: {info.TotalLength} (only available if the stream was sent with `SendFile`)
");
}The SDK exposes three interchangeable styles for awaiting asynchronous operations. Coroutines, async/await and UniTask.
1. Coroutines (default, no dependency) — shown throughout this README.
2. async/await (no dependency) — every operation returns an awaitable instruction (ConnectInstruction, PublishTrackInstruction, PerformRpcInstruction, the stream read instructions, …), so you can await it directly. As with coroutines, you inspect success/failure on the instruction (IsError) — await does not throw. Continuations resume on Unity's main thread.
async void Start()
{
var room = new Room();
var connect = room.Connect("ws://localhost:7880", "<join-token>", new RoomOptions());
await connect;
if (!connect.IsError)
Debug.Log("Connected to " + room.Name);
}Use
async voidonly for top-level event handlers (e.g. button callbacks); its exceptions surface to Unity's log rather than to a caller. Preferasync Task/async UniTaskVoidelsewhere.
3. UniTask (optional) — install UniTask (com.cysharp.unitask). The SDK auto-detects it via the LIVEKIT_UNITASK scripting define and enables the LiveKit.UniTask assembly, which adds CancellationToken support, composition, and async streams.
Cancellation (abandon-awaiter semantics — the underlying request is not cancelled on the wire):
await room.Connect("ws://localhost:7880", "<join-token>", new RoomOptions())
.AsUniTask(cancellationToken);Run operations in parallel. AsUniTask does not throw on failure (matching the
coroutine path), so keep the instructions and check IsError on each after the
await — otherwise a failed operation passes silently:
var publishCamera = room.LocalParticipant.PublishTrack(cameraTrack, cameraOptions);
var publishMicrophone = room.LocalParticipant.PublishTrack(microphoneTrack, microphoneOptions);
await UniTask.WhenAll(publishCamera.AsUniTask(ct), publishMicrophone.AsUniTask(ct));
if (publishCamera.IsError || publishMicrophone.IsError)
Debug.LogError("Failed to publish one or more tracks");Consume an incremental stream with await foreach. The sequence ends at end-of-stream; if the stream ends with an error it throws a StreamError:
try
{
await foreach (var chunk in reader.ReadIncremental().AsAsyncEnumerable(ct))
Process(chunk);
}
catch (StreamError e)
{
Debug.LogError(e.Message);
}Error-handling differs by API: awaiting an instruction (and
AsUniTask) never throws on a failed operation — you inspectIsErrorafter theawait. The stream enumerable is the exception:await foreachhas no post-loop point to checkIsError, so a mid-stream failure surfaces by throwingStreamError.
To enable verbose logging, define the LK_VERBOSE symbol:
- Navigate to Project Settings → Player
- Select your platform tab (e.g., Mac, iOS, Android).
- Under Other Settings → Scripting Define Symbols, add
LK_VERBOSE.
| LiveKit Ecosystem | |
|---|---|
| Agents SDKs | Python · Node.js |
| LiveKit SDKs | Browser · Swift · Android · Flutter · React Native · Rust · Node.js · Python · Unity · Unity (WebGL) · ESP32 · C++ |
| Starter Apps | Python Agent · TypeScript Agent · React App · SwiftUI App · Android App · Flutter App · React Native App · Web Embed |
| UI Components | React · Android Compose · SwiftUI · Flutter |
| Server APIs | Node.js · Golang · Ruby · Java/Kotlin · Python · Rust · PHP (community) · .NET (community) |
| Resources | Docs · Docs MCP Server · CLI · LiveKit Cloud |
| LiveKit Server OSS | LiveKit server · Egress · Ingress · SIP |
| Community | Developer Community · Slack · X · YouTube |
