Use AR51.Net in Your App — Hosting, UI Threads and Data Flow
The samples are console apps. Real apps — dashboards, back ends, desktop tools — need a few more patterns: one long-lived client, knowing what arrives by itself and what to ask for, data handed off from the receiving thread, and UI updates on the right thread.
What's pushed, what to poll
Some data arrives by itself; some you ask for. A dashboard needs both.
| Arrives by itself (events or live lists) | Ask for it (call when you need it, e.g. every 2–5 s for a status view) |
|---|---|
Skeletons and objects (SkeletonReceived, ObjectReceived) | Server program and tracking status (GetStatusAsync) |
Connection state (ConnectionStateChanged) | Cameras and their connection state (GetCamerasAsync) |
Server machines appearing and disappearing (ServersChanged) | Camera image settings (GetSettingsAsync) |
Devices connecting and disconnecting (DevicesChanged; battery levels refresh about every 10 s) | The tracked area (GetCaptureBoundsAsync) |
Recording state (RecordingStateChanged, checked about once a second) | |
Calibration and auto-tune progress (their …Started, …ProgressChanged and …Ended events) |
GetCamerasAsync leaves out cameras whose camera server didn't answer in time. In a status view, keep the last known entry and show it as unreachable rather than removing it.
One client for the whole app
Create one IAR51Client and keep it for the life of your application. In ASP.NET Core or a .NET worker service, register it as a singleton and connect from a background service, so a missing AR 51 system never blocks your app's startup:
using AR51.Net;
using Microsoft.Extensions.Hosting;
// Connects in the background and disconnects when the app stops.
public sealed class AR51Connection(IAR51Client ar51) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await ar51.StartAsync(stoppingToken); // returns once connected; keeps trying until then
}
catch (OperationCanceledException)
{
// the app is shutting down
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
await ar51.StopAsync(cancellationToken);
await base.StopAsync(cancellationToken);
}
}
Register it with the app's logging, and read the server address from configuration ("AR51": { "Host": "192.168.1.20" } in appsettings.json; leave it out to use discovery):
using AR51.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args); // or WebApplication.CreateBuilder(args) in ASP.NET Core
builder.Services.AddSingleton(services => AR51Client.Create(
new AR51ClientOptions { Host = builder.Configuration["AR51:Host"] },
services.GetRequiredService<ILoggerFactory>()));
builder.Services.AddHostedService<AR51Connection>();
await builder.Build().RunAsync();
The host disposes the client when it shuts down. Inject IAR51Client wherever you need it. (If you build a service provider yourself, dispose it with DisposeAsync — the client is async-disposable only.)
Stopping the client cancels a calibration this client started. Redeploying or restarting a back end in the middle of a calibration it started cancels that calibration — see Calibration.
Hand data off the receiving thread
SkeletonReceived and ObjectReceived run on the stream's background thread, and slow handlers back up the stream. Copy what you need and return. Two patterns cover most apps.
Keep the latest skeleton per person
For dashboards, views and web pushes that sample at their own rate, store the latest skeleton per person and read a snapshot whenever you need it:
using System.Collections.Concurrent;
var latest = new ConcurrentDictionary<string, Skeleton>();
ar51.Skeletons.SkeletonReceived += (_, e) => latest[e.Skeleton.Id] = e.Skeleton; // constant time; locks only briefly
// Elsewhere — a timer, a UI frame, a web request — read it. Enumerating the dictionary takes no locks
// (unlike .Values, which copies under every lock and makes the receiving thread wait).
foreach ((string id, Skeleton skeleton) in latest)
Console.WriteLine($"{id}: hips at {skeleton.GetPosition(Joint.MidHip)} m");
// Drop people you haven't heard from for a while (the timeout is your choice).
// Removing the exact entry you checked never deletes a newer skeleton that just arrived.
DateTimeOffset cutoff = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1);
foreach (KeyValuePair<string, Skeleton> entry in latest)
if (entry.Value.ReceivedTime < cutoff)
latest.TryRemove(entry);
Skeleton is immutable, so a snapshot can be read from any thread.
Process skeletons in order without blocking
For analytics that work through skeletons in order, queue them into a bounded channel and process on your own task. If processing falls behind, the oldest skeletons are dropped instead of stalling the stream. A buffer of 4096 holds about 1.7 seconds at 20 people and 120 frames per second:
using System.Threading.Channels;
long dropped = 0;
var queue = Channel.CreateBounded<Skeleton>(
new BoundedChannelOptions(4096) { FullMode = BoundedChannelFullMode.DropOldest, SingleReader = true },
itemDropped: _ => Interlocked.Increment(ref dropped)); // log this: drops mean your consumer is too slow
ar51.Skeletons.SkeletonReceived += (_, e) => queue.Writer.TryWrite(e.Skeleton);
await foreach (Skeleton skeleton in queue.Reader.ReadAllAsync(cancellationToken))
{
// heavy work here — on your task, not the receiving thread
}
Update a UI
Events are raised on background threads. To touch UI controls, marshal to the UI thread: capture the UI's SynchronizationContext on the UI thread (or use your framework's dispatcher — Dispatcher in WPF, DispatcherQueue in WinUI, InvokeAsync in Blazor):
// Run this on the UI thread, for example in your window's constructor.
SynchronizationContext ui = SynchronizationContext.Current
?? throw new InvalidOperationException("Capture the SynchronizationContext on the UI thread.");
ar51.ConnectionStateChanged += (_, e) =>
ui.Post(_ => Console.WriteLine($"Connection: {e.State}"), null); // e.g. statusText.Text = e.State.ToString()
For live skeletons, don't post every event to the UI. Render from the latest-skeleton snapshot on your UI's own frame tick.
Relay to a web front end
To show live data in a browser, keep the latest-skeleton snapshot on the server and push it at a fixed rate — for example 30 times a second over SignalR or a WebSocket — rather than forwarding every event. Bandwidth stays predictable however many people are tracked, and a slow browser never slows the stream.
System.Text.Json skips public fields by default, and System.Numerics vectors keep X, Y, Z in fields — so serializing a Skeleton directly gives empty joints, with no error. Copy what the browser needs into your own types first.
// What the browser receives: positions flattened to x, y, z per joint, in meters.
public sealed record SkeletonDto(string Id, string Name, float[] Positions, float[] Confidences);
using System.Collections.Concurrent;
using System.Text.Json;
var latest = new ConcurrentDictionary<string, Skeleton>();
ar51.Skeletons.SkeletonReceived += (_, e) => latest[e.Skeleton.Id] = e.Skeleton;
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / 30)); // 30 updates a second
while (await timer.WaitForNextTickAsync(cancellationToken))
{
DateTimeOffset cutoff = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1); // leave out people who have left
SkeletonDto[] frame = latest
.Where(entry => entry.Value.ReceivedTime >= cutoff)
.Select(entry => entry.Value)
.Select(s => new SkeletonDto(
s.Id,
s.EntityDisplayName,
s.Positions.SelectMany(p => new[] { p.X, p.Y, p.Z }).ToArray(),
s.Confidences.ToArray()))
.ToArray();
string json = JsonSerializer.Serialize(frame);
// send json over your WebSocket — or, with SignalR, send frame itself: await hub.Clients.All.SendAsync("skeletons", frame)
}
A complete example
The web dashboard sample puts these patterns together in an ASP.NET Core app: hosting, live skeleton frames pushed to the browser, server status, camera previews, calibration and recording.
See also
- Data conventions, threading and errors
- Connecting — reconnection, logging and shutdown
IAR51Client·ISkeletonClient