Livekit.Rtc.Dotnet
0.1.7
dotnet add package Livekit.Rtc.Dotnet --version 0.1.7
NuGet\Install-Package Livekit.Rtc.Dotnet -Version 0.1.7
<PackageReference Include="Livekit.Rtc.Dotnet" Version="0.1.7" />
<PackageVersion Include="Livekit.Rtc.Dotnet" Version="0.1.7" />
<PackageReference Include="Livekit.Rtc.Dotnet" />
paket add Livekit.Rtc.Dotnet --version 0.1.7
#r "nuget: Livekit.Rtc.Dotnet, 0.1.7"
#:package Livekit.Rtc.Dotnet@0.1.7
#addin nuget:?package=Livekit.Rtc.Dotnet&version=0.1.7
#tool nuget:?package=Livekit.Rtc.Dotnet&version=0.1.7
Livekit.Rtc.Dotnet
.NET SDK to integrate LiveKit's real-time video, audio, and data capabilities into your .NET applications using WebRTC. Access the same powerful API offered by client SDKs, but directly from your .NET server: connect to rooms as a participant, publish and subscribe to audio/video tracks, send and receive data messages, perform RPC calls, and more.
This SDK does not provide Server APIs to manage LiveKit resources such as Rooms, Egress, Ingress or SIP. If you want to manage LiveKit APIs from your .NET backend, please check Livekit.Server.Sdk.Dotnet instead.
Design principles
This library is crafted based on three core principles:
- Livekit.Rtc.Dotnet uses the C# auto-generated protobuf implementation of LiveKit RTC protocol. This ensures compatibility and ease of maintenance as the API evolves.
- Livekit.Rtc.Dotnet uses C# FFI bindings to the LiveKit Rust SDK. This allows us to leverage existing, well-tested code for WebRTC functionality that is already in use in other LiveKit SDKs. It brings the benefits of native performance for any OS platform while providing a common .NET interface. The library includes precompiled native binaries for Windows, Linux, and macOS, for both x64 and ARM64 architectures.
- Livekit.Rtc.Dotnet maintains full feature parity with other LiveKit RTC SDKs for the server side, especially the two more mature ones: Node.js and Python.
Features
Real-Time Media
Publish and subscribe to audio/video tracks with full programmatic control. Process frames in real-time, mix audio sources, publish pre-encoded video without re-encoding it, and build custom media pipelines. Perfect for server-side processing.
Data & Communication
Send reliable or lossy data messages, stream application frames over data tracks, implement RPC request-response patterns, and build real-time chat with topic-based channels. Full control over data flow between participants.
Room & Participant Management
Active speaker detection, transcription support, connection quality monitoring, permission changes, and 30+ event types for comprehensive room state management.
Security & Encryption
Built-in end-to-end encryption (E2EE) with AES-GCM, key rotation, and per-participant key management. Keep your sensitive communications secure, also in your .NET server.
Production-Ready
Automatic reconnection, multi-room support, isolated contexts, and battle-tested WebRTC implementation from the LiveKit Rust SDK.
Installation
dotnet add package Livekit.Rtc.Dotnet
Usage
Basic Connection
using LiveKit.Rtc;
var room = new Room();
await room.ConnectAsync("wss://your-livekit-server.com", "your-access-token",
new RoomOptions { AutoSubscribe = true });
Console.WriteLine($"Connected to {room.Name}");
await room.DisconnectAsync();
Publishing Audio
var audioSource = new AudioSource(48000, 1); // 48kHz, mono
var audioTrack = LocalAudioTrack.Create("my-audio", audioSource);
var publication = await room.LocalParticipant!.PublishTrackAsync(audioTrack);
// Capture audio frames
var audioData = new short[480]; // 10ms at 48kHz
var audioFrame = new AudioFrame(audioData, 48000, 1, 480);
await audioSource.CaptureFrameAsync(audioFrame);
Publishing Video
var videoSource = new VideoSource(1920, 1080);
var videoTrack = LocalVideoTrack.Create("my-video", videoSource);
await room.LocalParticipant!.PublishTrackAsync(videoTrack, new TrackPublishOptions
{
VideoCodec = Proto.VideoCodec.H264, // VP8 (default), H264, AV1, VP9...
Simulcast = true,
Source = Proto.TrackSource.SourceCamera,
});
// Capture video frames
var videoData = new byte[1920 * 1080 * 4]; // RGBA
var videoFrame = new VideoFrame(1920, 1080, Proto.VideoBufferType.Rgba, videoData);
videoSource.CaptureFrame(videoFrame);
Receiving Media
room.TrackSubscribed += async (sender, e) => {
if (e.Track is RemoteVideoTrack videoTrack) {
using var videoStream = new VideoStream(videoTrack);
await foreach (var frame in videoStream.WithCancellation(cts.Token)) {
// Process video frame
Console.WriteLine($"Frame: {frame.Frame.Width}x{frame.Frame.Height}");
}
}
};
Event Handling
room.ParticipantConnected += (sender, participant) => {
Console.WriteLine($"{participant.Identity} joined");
};
room.ActiveSpeakersChanged += (sender, e) => {
Console.WriteLine($"Active speakers: {e.Speakers.Count}");
};
room.DataReceived += (sender, e) => {
var message = Encoding.UTF8.GetString(e.Data);
Console.WriteLine($"Data from {e.Participant.Identity}: {message}");
};
room.ParticipantPermissionsChanged += (sender, e) => {
Console.WriteLine($"{e.Participant.Identity} can publish: {e.Permissions?.CanPublish}");
};
room.LocalTrackRepublished += (sender, e) => {
// After a full reconnect the publication keeps its object but gets a new SID
Console.WriteLine($"{e.PreviousSid} is now {e.Publication.Sid}");
};
room.ParticipantDisconnected += (sender, participant) => {
Console.WriteLine($"{participant.Identity} left: {participant.DisconnectReason}");
};
room.Disconnected += (sender, reason) => {
// ClientInitiated, RoomDeleted, DuplicateIdentity, ParticipantRemoved...
// LocalParticipant and RemoteParticipants are still readable here
Console.WriteLine($"Disconnected: {reason}");
};
End-to-End Encryption
var key = new byte[32]; // AES-256 key
var options = new RoomOptions {
E2EE = new E2EEOptions {
KeyProviderOptions = new KeyProviderOptions { SharedKey = key }
}
};
await room.ConnectAsync(url, token, options);
// All tracks automatically encrypted/decrypted
RPC Communication
// Register RPC method handler
room.LocalParticipant!.RegisterRpcMethod("greet", async (data) => {
return $"Hello, {data.CallerIdentity}!";
});
// Call RPC method on another participant
var response = await room.LocalParticipant!.PerformRpcAsync(
destinationIdentity: "other-participant",
method: "greet",
payload: "{}",
responseTimeout: 5
);
Text and Byte Streams
// Receiver: handlers run on the thread pool, so they may also read synchronously
room.RegisterTextStreamHandler("chat", async (reader, senderIdentity) => {
try {
var text = await reader.ReadAllAsync();
Console.WriteLine($"{senderIdentity}: {text}");
} catch (StreamException) {
// The sender or the room disconnected before the stream was closed
}
});
// Sender: multi-byte characters are never split across chunks
await using var writer = await room.LocalParticipant!.StreamTextAsync(topic: "chat");
await writer.WriteAsync("Hello 🎉"); // closed (trailer sent) when disposed, or with CloseAsync()
Data Tracks
Data tracks stream application frames (telemetry, sensor data, game state...) from one publisher to any number of subscribers. Unlike data messages they are a named, subscribable track; frames are not buffered for subscribers that join later.
// Publisher
var dataTrack = await room.LocalParticipant!.PublishDataTrackAsync("telemetry");
dataTrack.TryPush(new DataTrackFrame(payload, userTimestamp: 42)); // PushFrameException if it cannot be sent
dataTrack.Unpublish();
// Subscriber
room.DataTrackPublished += async (sender, remoteTrack) => {
Console.WriteLine($"{remoteTrack.PublisherIdentity} published {remoteTrack.Info.Name}");
using var stream = remoteTrack.Subscribe();
await foreach (var frame in stream) { // ends when the track is unpublished
Console.WriteLine($"{frame.Payload.Length} bytes, timestamp {frame.UserTimestamp}");
}
};
room.DataTrackUnpublished += (sender, e) => Console.WriteLine($"{e.Sid} unpublished");
Publishing Pre-encoded Video
Publish video that is already encoded (for example H.264 from a hardware encoder or a demuxed file) without decoding and re-encoding it:
using var source = new EncodedVideoSource(1280, 720);
var track = LocalVideoTrack.Create("camera", source);
await room.LocalParticipant!.PublishTrackAsync(track, new TrackPublishOptions {
VideoCodec = Proto.VideoCodec.H264,
VideoEncoder = Proto.VideoEncoderBackend.EncoderBackendPreEncoded,
Simulcast = false,
});
// Feed complete access units at the playback rate (Annex B for H.264/H.265)
source.CaptureFrame(new EncodedVideoFrame(
accessUnit, 1280, 720, Proto.VideoCodec.H264, Proto.EncodedFrameType.EncodedFrameKey, timestampUs));
// Forward encoder feedback (key frame requests, bitrate targets) to your encoder
var feedback = source.TakeFeedback();
if (feedback.KeyframeRequested) encoder.RequestKeyFrame();
Controlling Remote Tracks
room.TrackSubscribed += (sender, e) => {
var publication = e.Publication;
publication.SetVideoQuality(Proto.VideoQuality.Low); // simulcast layer (video only)
publication.SetVideoDimensions(640, 360); // let the server pick the layer for this size
publication.SetEnabled(false); // pause receiving without unsubscribing
};
Simulating Reconnections
Room.SimulateScenarioAsync forces, from a test, the reconnection paths that are otherwise only reachable through network failures. The task completes once the SDK has started the scenario; the reconnection itself is observed through the room events.
// Full reconnect: the SDK rebuilds its session and republishes the local tracks.
// Raises Reconnecting, then one LocalTrackRepublished per local track, then Reconnected.
await room.SimulateScenarioAsync(Proto.SimulateScenarioKind.SimulateFullReconnect);
// Resume: only the signaling connection is re-established and the publications are kept.
await room.SimulateScenarioAsync(Proto.SimulateScenarioKind.SimulateSignalReconnect);
See Proto.SimulateScenarioKind for the other variants (server-initiated leave, node failure, migration...).
Example apps
At LivekitRtc.Example you can find several example applications demonstrating the usage of LiveKit.Rtc.Dotnet SDK. See the examples README for more details.
For developers of the SDK
Clone repository
Make sure to clone with submodule option:
git clone --recurse-submodules https://github.com/pabloFuente/livekit-server-sdk-dotnet.git
Run tests
All E2E tests automatically launch necessary services as Docker containers with Testcontainers.
dotnet test LivekitRtc.Tests
Building from source
Prerequisites:
- .NET 10.0 SDK (
global.jsonpins it; the package itself targetsnetstandard2.1) - protoc
- Rust (only if also building native libraries from source)
1. Get native FFI libraries
Option 1: download pre-compiled binaries from LiveKit Rust SDK releases
The easiest way to get the latest FFI binaries without building from source:
# From LivekitRtc directory
./download_ffi.sh # Downloads latest version
./download_ffi.sh 0.12.44 # Or specify a version
This downloads pre-compiled binaries from LiveKit Rust SDK releases for all supported platforms (Windows, Linux, macOS on x64 and ARM64).
Option 2: building native libraries from source
If you want to modify the Rust/FFI code or use unreleased features. You will need:
- .NET 10.0 SDK (
global.jsonpins it; the package itself targetsnetstandard2.1) - Rust installed with rustup. No specific version needs to be installed beforehand:
rust-sdkspins its toolchain in filerust-toolchain.toml(e.g. Rust 1.97.1 forlivekit-ffi/v0.13.4), and rustup installs it automatically the first timecargoruns insiderust-sdks. - protoc
- The platform-specific build dependencies listed in the rust-sdks README (e.g. clang 21+ and some system libraries on Linux).
The Rust build needs the nested submodules of rust-sdks (livekit-protocol/protocol and yuv-sys/libyuv). Option --checkout makes sure they are checked out, even if they have been disabled locally (see the note below):
# From the root directory (livekit-server-sdk-dotnet/.)
git submodule update --init --recursive --checkout LivekitRtc/rust-sdks
cd LivekitRtc/rust-sdks/livekit-ffi
cargo build --release
The native library is generated in the workspace target folder LivekitRtc/rust-sdks/target/release/ (liblivekit_ffi.so on Linux, liblivekit_ffi.dylib on macOS, livekit_ffi.dll on Windows). Copy it to the appropriate runtimes/{rid}/native/ folder. For example, on Linux x64:
# From LivekitRtc directory
cp rust-sdks/target/release/liblivekit_ffi.so runtimes/linux-x64/native/
Take into account that Rust builds for the host platform by default, so you may need to cross-compile for other platforms.
2. Generate Protocol Buffers
# From the root directory (livekit-server-sdk-dotnet/.)
./generate_proto.sh
3. Build the .NET SDK
# From LivekitRtc directory
dotnet build
Upgrade version of rust-sdks
The /sync-rust-sdks skill runs this whole procedure, including exposing the new FFI features, and the sync-rust-sdks workflow runs it on every new GA livekit-ffi release and opens a pull request. See AI coding agents.
To upgrade the version of the rust-sdks Git submodule:
cd LivekitRtc/rust-sdks
git fetch --all
git checkout <VERSION_TAG> # e.g. git checkout livekit-ffi/v0.12.76
cd ../..
./generate_proto.sh
dotnet pack -c Debug -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
./download_ffi.sh <VERSION_TAG> # e.g. ./download_ffi.sh livekit-ffi/v0.12.76
Then commit:
git commit -m "Update rust-sdks to <VERSION>"
git push
Perform release
- Create a commit like this updating the version.
- Make sure to run script
./build_local.shto properly format all files with csharpier. Commit any changes in the repo after the script finishes. - Create a new release in GitHub with:
- Release title
rtc-X.Y.Z - A new tag
rtc-X.Y.Z - Description with relevant changes (e.g.,
Update to LiveKit Rust SDK vA.B.C)
- Release title
The rtc- prefix in the tag is mandatory for the publish workflow to identify that it is a release for package Livekit.Rtc.Dotnet specifically.
After creating the release, workflow publish.yml will automatically publish the new version to NuGet and will perform the necessary post-release tasks.
AI coding agents
CLAUDE.md briefs AI coding agents on this repository, including how the FFI layer works. For this package the relevant tool is the /sync-rust-sdks Claude Code skill (SKILL.md), which carries out the rust-sdks upgrade end to end:
Start from a clean working tree with
protoc36.2,protoc-gen-twirpcsv1.1.0 (generate_proto.shalso regenerates the server package),gh,curlandunzipinstalled, and Docker running for the e2e tests.Open Claude Code at the repository root and run the skill with the target
livekit-ffiversion, or without one to take the latest release:/sync-rust-sdks 0.13.4The skill checks that the GitHub release carries the six native binaries, moves the submodule, reads the upstream changelog and the FFI proto diff, regenerates
LivekitRtc/Protowith./generate_proto.sh, downloads the binaries withdownload_ffi.sh(it asks before overwriting them), fixes the build and runs the parity guards. It then stops at a checkpoint listing the relevant upstream changes, the proto members that forced fixes, and every new FFI request, callback, room event and option field with a proposal to expose it (with its Node and Python name) or to ignore it. Strike what you do not want and confirm.It exposes the confirmed features following the Node and Python RTC SDKs, with their mapping or e2e tests, README sections and changelog bullets under
## Unreleased, then runs the no-Docker tests and the e2e classes for the areas the upstream diff touched, and builds and checks the API reference.It ends with the test results (any
FFI Panicor[WARN] RoomEventline means the binaries and the protos disagree), the open decisions and the suggested commits, and never commits. Review the diff, commit asUpdate "rust-sdks" to livekit-ffi/vX.Y.Z(the submodule pointer,LivekitRtc/Proto, the six binaries and anygenerate_proto.shchange) andAdapt LivekitRtc to livekit-ffi/vX.Y.Z(everything else), and follow Perform release when ready.
The sync-rust-sdks workflow runs the same skill unattended: every day it looks for a GA livekit-ffi release newer than the submodule whose six binaries are published and, when it finds one, downloads the binaries, runs the skill and opens a pull request with those two commits and the skill's report as description. Instead of stopping at the checkpoint, it exposes the features that the Node or Python SDK exposes and lists the rest as open decisions. Its header lists the secrets and repository settings it needs, and it can be started by hand from the Actions tab to choose the Claude model and effort level.
A hook keeps Proto/*.g.cs, runtimes/ and the submodule out of reach of agent edits. The coverage script also works on its own, from the repository root. It lists the FfiRequest, FfiEvent and RoomEvent cases that the hand-written sources do not send, read or handle yet:
python3 .claude/skills/sync-rust-sdks/scripts/ffi_coverage.py
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Google.Protobuf (>= 3.36.2)
- System.Threading.Channels (>= 10.0.14)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Livekit.Rtc.Dotnet:
| Package | Downloads |
|---|---|
|
Fluxer.Net.Voice
Voice channel support for Fluxer.Net bots: joining voice channels and sending audio. |
|
|
Fluxify.Voice
Fluxify is a .NET Library for building chat bots for the fluxer platform. |
GitHub repositories
This package is not used by any popular GitHub repositories.
# Changelog (Release Notes)
## 0.1.7
- Update rust-sdks to [livekit-ffi/v0.13.4](https://github.com/livekit/rust-sdks/releases/tag/livekit-ffi%2Fv0.13.4) [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- Fix the process aborting when a remote participant publishes a transcription whose track SID
is not a valid track SID (an empty string, for example): the native client panicked while
handling it. The transcription is now dropped with a native warning, and
`Room.TranscriptionReceived` is not raised for it. [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- A failed native request no longer prints the whole request to stderr in its
`failed to handle request` error line, so the key passed to a failing
`KeyProvider.SetSharedKey` or `KeyProvider.SetKey` call stays out of the console and the
logs. [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- Publishing a video track with a `TrackPublishOptions.VideoCodec` that no encoder on the host
can produce (H.265 without an H.265 encoder, for example) now publishes VP8 and logs the
native warning `no video encoder on this host can produce H265; publishing VP8 instead`,
instead of announcing a codec the host cannot encode. Pre-encoded publishing
(`EncodedVideoSource` with `VideoEncoder = EncoderBackendPreEncoded`) no longer needs a local
encoder for its codec, so pre-encoded H.265 works on any host. [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- Fix `EncodedVideoFrame.Metadata` (sent when `TrackPublishOptions.FrameMetadataFeatures` is
set) reaching subscribers on the wrong frame when frames share a timestamp or are captured
within the same millisecond: the sender looked the metadata up by capture time truncated to
the millisecond, so such frames carried another frame's metadata. [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- Reconnection now spaces its ten attempts like the JavaScript client (waits of 0.3 s, 1.2 s,
2.7 s, 4.8 s, then 7 s, plus up to 1 s of jitter), so a `Room` rides out a network outage of
about 45 s before it gives up and raises `Disconnected`; the previous random waits could spend
every attempt within a few seconds. No new attempt starts once reconnection has run for 60 s,
so a room no longer stays in `Reconnecting` for minutes, and a join or reconnect whose
response times out now closes its WebSocket instead of leaving it open. [PR #148](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/148)
- XML documentation comments for every public member, with parameter and return value
descriptions on every method, for IntelliSense and for the new
[API reference](https://pablofuente.github.io/livekit-server-sdk-dotnet/) generated with DocFX.
[commit f5660c2](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f5660c2f0225936951568c0c3ab403175521063a)
- Fix wrong XML documentation: `LocalParticipant.PerformRpcAsync` waits 15 seconds for the response
by default, not 10; `Room.E2EEStateChanged` fires on every encryption state change, not only on
errors; this version never calls `FrameProcessor.OnStreamInfoUpdated` and `OnCredentialsUpdated`,
and never throws `FfiException` or `TrackException` (a failed native request raises
`InvalidOperationException`, a server error `RoomException`). The defaults of `RoomOptions`,
`TrackPublishOptions.Simulcast`, `DataPublishOptions.Reliable`, `E2EEOptions` and
`KeyProviderOptions` are now documented, as is what each `Dispose` releases, and `Room` gains a
usage example. The API reference lists only the `LiveKit.Proto` types the public API uses.
## 0.1.6
- Fix the process terminating (`FFI Panic` followed by exit code 1) in two situations: calling
`LocalParticipant.UnpublishTrackAsync` with something that is not a track SID (a track name,
an empty string, a remote track's SID, which was always empty), which now throws
`ArgumentException`; and cancelling or timing out `Room.ConnectAsync` once the FFI had
connected, which left a handshake unanswered that the FFI waits 15 s for before panicking.
The SDK now completes that handshake and leaves the room in the background. In the same
spirit, a `PublishTrackAsync` cancelled after the track was published now unpublishes it
instead of leaving a track on the server the SDK does not know about, and an FFI callback
that races with a cancelled wait is kept for the reconciliation instead of being dropped. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `Room.Disconnected` and `ConnectionStateChanged` (with `ConnDisconnected`) being raised
twice on every `DisconnectAsync` (once from the FFI's own disconnected event, once from the
method), always with `UnknownReason`. They are raised once, with the real reason
(`ClientInitiated`, `RoomDeleted`, `DuplicateIdentity`, `ParticipantRemoved`...), and
`Room.LocalParticipant` and `Room.RemoteParticipants` stay readable inside the `Disconnected`
handlers (they were cleared before the handlers ran). `ConnectAsync` no longer reports a
spurious `ConnDisconnected` first, and `ConnectionStateChanged` carries the state at the time
of the change: a slow handler used to make `ConnReconnecting` vanish from the sequence. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `Room.TrackUnsubscribed`, `TrackMuted` and `TrackUnmuted` being dropped (logged as
`[WARN] RoomEvent.TrackUnsubscribed: Participant ... not found`) when the remote participant
leaves the room: the lookup ran on the event chain after `ParticipantDisconnected` had already
removed the participant. Remote track events are now resolved on arrival. `LocalTrackPublished`
and `LocalTrackSubscribed` no longer poll for the publication inside the event chain (up to
500 ms of stall per miss, and a dropped event under thread-pool starvation): they wait for
`PublishTrackAsync` to register it. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `RemoteTrack.Sid`, `Name` and `IsMuted` never being set (the `TrackSubscribed` track
always reported an empty SID), and local tracks reporting `TR_unknown` after
`PublishTrackAsync` instead of their publication's SID. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix text data streams corrupting multi-byte characters (`€`, accented letters, CJK, emoji)
that fell on a chunk boundary: chunks were cut every 15000 bytes regardless of UTF-8
boundaries and decoded one by one, so a replacement character appeared at every boundary.
Chunks are now split on character boundaries, and the reader joins a character split by
another sender. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `TextStreamReader` and `ByteStreamReader` reads (`ReadAllAsync`, `await foreach`) waiting
forever when the sender or the room disconnected before the stream was closed: they now throw
`StreamException` once the buffered chunks are consumed. Text and byte stream handlers run on
the thread pool instead of the FFI callback thread: a handler that blocked on `ReadAllAsync`
deadlocked, and stalled every FFI event of the process. `TextStreamWriter.Dispose()` and
`ByteStreamWriter.Dispose()` now send the trailer (`using var writer = ...` used to leave the
remote reader open forever; only `DisposeAsync` closed it); new
`CloseAsync(reason, attributes, CancellationToken)` overloads; `StreamInfo.Size` is reported
for an announced size of 0. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `FrameCryptor.SetEnabled` and `SetKeyIndex` failing with
`InvalidOperationException: FFI request failed: invalid handle`: the requests did not carry
the track SID the FFI addresses the cryptor by. `FrameCryptor` now exposes `TrackSid` (set by
`E2EEManager.GetFrameCryptors`) and has a constructor overload taking it. New
`KeyProviderOptions.KeyDerivationFunction` (defaults to PBKDF2, as before); a null
`RatchetSalt` now falls back to the default salt instead of throwing. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `AudioSource.CaptureFrameAsync` throwing `TimeoutException` for frames longer than about
10 s (a TTS clip, a file): the FFI only acknowledges a frame once it fits its queue, so the
wait is now proportional to the frame. Fix `WaitForPlayoutAsync` never completing when another
frame was captured meanwhile, and hanging after `Dispose`; cancelling one wait no longer
cancels the others. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `AudioResampler.Flush()` returning a frame with zero samples before any audio was pushed,
and ignoring resampling errors. `Push(AudioFrame)` now rejects frames with another channel
count or sample rate (the output used to be garbage of half the duration), and push and flush
are safe to call from several threads. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix `DataTrackStream.ReadAsync` throwing `ChannelClosedException` when the stream is disposed
while a read is pending: it returns null, like at the end of the stream. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Fix RPC: `RpcError.RpcData` was `""` instead of null when the remote error carried no data;
the message and the full stack trace of an exception thrown by an RPC handler were sent to
the remote caller, now only the built-in `Application error in method handler` crosses the
wire and the exception is logged locally with `[ERROR]`, as the Node and Python SDKs do; and
`PerformRpcAsync` surfaces the FFI's `RpcError` with `ResponseTimeout` instead of a generic
`TimeoutException` when `responseTimeout` elapses. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Media delivery: `AudioStream` bounded buffers (`capacity > 0`) drop the oldest frame instead
of the newest when the consumer falls behind, like `VideoStream` and the Python SDK; a frame
processor that throws is logged with `[WARN]` (`Debug.WriteLine` was silent in Release
builds); audio and video frames and data packets are block-copied out of native memory (a
byte loop on the FFI callback thread delayed every other event, 3 MB per 1080p frame); the
frames the native side still emits right after a stream is disposed are released instead of
leaked; and `AudioMixer` reads its sources concurrently (N stalled sources cost one timeout,
not N) and keeps a source that stalls for one round instead of removing it for good. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Participants: new `Participant.Attributes` (kept current by `ParticipantAttributesChanged`,
which left it stale), `Participant.State` and `Participant.DisconnectReason` (set before
`ParticipantDisconnected` is raised). `Participant.TrackPublications` returns a snapshot:
enumerating it while a track event was handled could throw `Collection was modified`.
`LocalTrackPublication.WaitForSubscriptionAsync` continuations no longer run inline on the
room's event chain (delaying the following events), and the wait is cancelled when the room
disconnects instead of hanging forever. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
- Rooms and the FFI client: a connected `Room` that is never disposed no longer stays
referenced by the process-wide FFI client (it listens from `ConnectAsync` to the disconnect);
`Room.Dispose()` now delivers the teardown events like `DisconnectAsync` does; the FFI event
queue no longer fills up with media frames, which made every request wait scan a thousand
stale events and could evict a pending callback (spurious `TimeoutException` under media
load); one throwing event subscriber no longer hides the event from the other rooms and
streams; and handler exceptions are logged with their type and stack trace, not only the
message. `FfiClient.CopyBuffer` threw `EntryPointNotFoundException` (the native library has
no such export) and is now implemented in managed code; `FfiClient.RetrievePtr` has the same
problem and cannot be fixed (a pointer to a managed array is only valid while pinned), so it
is marked `[Obsolete]`. [commit f2765fc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/f2765fc14a0a5325be47fae3501a8fc866d53970)
## 0.1.5
- Update rust-sdks to [livekit-ffi/v0.13.3](https://github.com/livekit/rust-sdks/releases/tag/livekit-ffi%2Fv0.13.3) ([commit 1001476](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/10014763a5212503352f2a39477ae86acf8add87))
- Fix process crash (native panic `index out of bounds: the len is 0 but the index is 0`) when
converting or capturing video frames in the `I420A`, `I422`, `I444` and `I010` formats.
`VideoFrame` only described the planes of `I420` and `NV12` buffers to the FFI, so the native
side found no planes for the other planar formats. All planar formats are now described from a
single plane table that is shared with `VideoFrame.GetPlane`, which also makes `GetPlane` work for
`I010`. [commit 026961e](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/026961e5fa1367fdac9d1954db456cd2f374b4e3)
- Fix a possible native access violation (process crash) when capturing audio or video frames.
The frame's managed sample/pixel array was only pinned while the pointer handed to the FFI was
built, not while the capture request was serialized and sent, so a compacting garbage
collection in between could move the array and leave the native side reading a stale pointer.
`AudioSource.CaptureFrameAsync`, `VideoSource.CaptureFrame` and `VideoFrame.Convert` now keep
the buffer (and, for planar video formats, every plane pointer) pinned through request
serialization and the native call. [PR #143](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/143) (audio) and
[PR #144](https://github.com/pabloFuente/livekit-server-sdk-dotnet/pull/144) (video).
- Fix `Room.LocalTrackUnpublished` being dropped (logged as
`[WARN] RoomEvent.LocalTrackUnpublished: Publication ... not found`) when the FFI
`UnpublishTrack` callback was processed before the matching `LocalTrackUnpublished` room
event. `LocalParticipant.UnpublishTrackAsync` now hands the removed publication over to the
room event instead of dropping it, and the event handler no longer retries the lookup, which
used to stall the room's FIFO event chain for up to 500ms per miss and delay every following
event. Most visible when unpublishing many tracks at once. [commit 18d3966](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/18d3966a9c742e0f99a659b0d40ec0b538dd43f2)
- The SDK version that the FFI sends to the LiveKit server as the client SDK version now
follows the package version in `LivekitRtc.csproj`. It was hard-coded to `0.1.0`.
[commit 8ce5332](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/8ce53323aedfa79d35091d3824e5ea982c7c3a7c)
- The NuGet package now embeds this changelog as its release notes, as `Livekit.Server.Sdk.Dotnet`
already did. [commit 880a38e](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/880a38e82783fa5a797e80f82af94f0286f0cf66)
- Data tracks, reaching parity with the Python RTC SDK: `LocalParticipant.PublishDataTrackAsync`
returns a `LocalDataTrack` to push `DataTrackFrame`s on. Remote tracks arrive through the new
`Room.DataTrackPublished` and `Room.DataTrackUnpublished` events (and are listed in
`Room.RemoteDataTracks`); `RemoteDataTrack.Subscribe()` returns a `DataTrackStream` that can
be read with `await foreach` and ends when the track is unpublished. [commit a3147e8](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/a3147e8923ed76611550bd38050ed18588f6164b)
- New `EncodedVideoSource` to publish pre-encoded video (H.264, VP8, ...) without re-encoding it:
`LocalVideoTrack.Create(name, encodedSource)`, `CaptureFrame(EncodedVideoFrame)` and
`TakeFeedback()` for the key-frame and bitrate requests to forward to the encoder. [commit a3147e8](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/a3147e8923ed76611550bd38050ed18588f6164b)
- New subscriber-side controls on `RemoteTrackPublication`: `SetVideoQuality` (simulcast layer),
`SetEnabled` (pause receiving without unsubscribing) and `SetVideoDimensions`. [commit a3147e8](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/a3147e8923ed76611550bd38050ed18588f6164b)
- New room events `ParticipantPermissionsChanged` (with the new `Participant.Permissions`
property), `ParticipantActive`, `LocalTrackRepublished` (local publications are now updated in
place and re-keyed after a full reconnect instead of going stale) and
`DataChannelBufferedAmountLowThresholdChanged`, raised after the new
`LocalParticipant.SetDataChannelBufferedAmountLowThreshold`. Bulk participant updates sent by
the server are now applied to the participant objects. [commit a3147e8](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/a3147e8923ed76611550bd38050ed18588f6164b)
- New `Room.SimulateScenarioAsync(Proto.SimulateScenarioKind)` to force the reconnection paths
from tests without touching the network: a full reconnect (the SDK rebuilds its session and
republishes its local tracks, raising `LocalTrackRepublished`), a resume, a server-initiated
leave... It mirrors `Room.simulate_scenario` in the Python SDK and `Room.simulateScenario` in
the Node SDK. See
[Simulating Reconnections](https://github.com/pabloFuente/livekit-server-sdk-dotnet/blob/main/LivekitRtc/README.md#simulating-reconnections). [commit a3147e8](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/a3147e8923ed76611550bd38050ed18588f6164b)
## 0.1.4
- Update rust-sdks to [livekit-ffi/v0.12.76](https://github.com/livekit/rust-sdks/releases/tag/livekit-ffi%2Fv0.12.76) ([commit 36077cc](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/36077cc273bd2a8c0952aafc818ca65c77e2af53))
- Add all the missing `TrackPublishOptions` fields, reaching parity with the Node/Python/Rust
RTC SDKs: `VideoCodec` (select the codec of published video tracks: VP8, H264, AV1, VP9...),
`Dtx`, `Red`, `Stream`, `PreconnectBuffer`, `FrameMetadataFeatures`, `ScalabilityMode`
(SVC publishing for VP9/AV1), `VideoEncoder` (preferred encoder backend) and
`DegradationPreference`. All new options are nullable and unset keeps the previous
behavior (the SDK defaults). The `TrackPublishOptions` → `Proto.TrackPublishOptions`
mapping is now covered by unit tests, including a parity guard that fails when a future
FFI proto regeneration introduces fields not exposed by the SDK. [commit 3e3cfb4](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/3e3cfb459794d2b031d69bc58933bc833c2cb8d5)
## 0.1.3
- Update rust-sdks to [livekit-ffi/v0.12.60](https://github.com/livekit/rust-sdks/releases/tag/livekit-ffi%2Fv0.12.60) ([commit 65e5fc4](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/65e5fc4accdd8060cc8994c8e672161a9c4d9bd2))
## 0.1.2
- Update rust-sdks to [livekit-ffi/v0.12.50](https://github.com/livekit/rust-sdks/releases/tag/livekit-ffi%2Fv0.12.50) ([commit 5225cc6](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/5225cc60c9acaf98fbced1f11193b50f9cc2a228))
## 0.1.1
- Update livekit/protocol to [v1.45.1](https://github.com/livekit/protocol/releases/tag/%40livekit%2Fprotocol%401.45.1) ([commit b6f0054](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/b6f00545ef4f9c87f4b6e1915f140d7e0f6e512d))
## 0.1.0
- Initial release of Livekit.Rtc.Dotnet SDK. [commit d110d6c](https://github.com/pabloFuente/livekit-server-sdk-dotnet/commit/d110d6c21d40b58f4af7f7f68f12e4d80d5dde93)