MDH.Videotiming.Connector
1.3.0
dotnet add package MDH.Videotiming.Connector --version 1.3.0
NuGet\Install-Package MDH.Videotiming.Connector -Version 1.3.0
<PackageReference Include="MDH.Videotiming.Connector" Version="1.3.0" />
<PackageVersion Include="MDH.Videotiming.Connector" Version="1.3.0" />
<PackageReference Include="MDH.Videotiming.Connector" />
paket add MDH.Videotiming.Connector --version 1.3.0
#r "nuget: MDH.Videotiming.Connector, 1.3.0"
#:package MDH.Videotiming.Connector@1.3.0
#addin nuget:?package=MDH.Videotiming.Connector&version=1.3.0
#tool nuget:?package=MDH.Videotiming.Connector&version=1.3.0
MDH.Videotiming.Connector
.NET client for MDH Videotiming. Push competition structure, send plunger times, read back official times judged on video.
Targets netstandard2.0, net48, net8.0-windows. The WPF status bar item and
settings editor are on the two Windows targets only.
Install
dotnet add package MDH.Videotiming.Connector
Quick start
var settings = Settings.LoadFromFile(@"C:\ProgramData\YourApp\videotiming.xml");
if (!settings.IsConfigured)
{
Log(settings.ConfigurationProblem);
return;
}
using var client = new VideotimingClient(
settings,
competitionExternalId: "1742",
competitionId: storedCloudId);
await client.SyncStructureAsync(structure, new StructurePushOptions
{
Roster = RosterMode.AddOnly,
RefuseIfJudged = true,
CreateIfMissing = true,
});
using var sub = client.Subscribe(e => { if (e.AffectsResults) QueueRefresh(); });
Times
| Time | Source | Read | Write |
|---|---|---|---|
| Official | judge marking a video frame | yes | no — no method exists |
| Plunger | timing hardware | yes | yes |
Official times may return Provisional = true (a coarse live mark). Use
OfficialTimesPage.Publishable to exclude those, non-finishes, and entries with no
elapsed time.
Settings
| Field | Default | Description |
|---|---|---|
Enabled |
false |
When off, every call is a no-op. |
BaseUrl |
https://videotiming.mdh.technology |
Control-plane base URL. |
OrganisationId |
(empty) | org_… — pick it after signing in (one organisation is prefilled). |
AuthToken |
(empty) | Issued by signing in with an MDH account (VideotimingSignIn). The only credential. Stored in clear. |
SignedInAs |
(empty) | Who signed in — display only. |
TimeoutSeconds |
30 |
Per-request timeout. |
PollSeconds |
15 |
Poll cadence when UseLiveStream is off. |
UseLiveStream |
true |
Use the WebSocket instead of polling. |
ClientName |
MDH Videotiming Connector |
Reported to the server. |
SourcePrefix |
host |
Namespace for minted external ids. Set once. |
AllowClearingVideoTimes |
false |
Required for ClearVideoTimeAsync. |
IsConfigured / ConfigurationProblem report whether the settings are usable.
IsSignedIn says whether a sign-in token is held.
Signing in
// Opens the system browser on the server's sign-in, listens on a loopback port for the
// one-time code it comes back with, and exchanges it. Nothing here sees a password.
var signedIn = await VideotimingSignIn.SignInAsync(settings);
settings.AuthToken = signedIn.Token;
settings.SignedInAs = signedIn.User.Email;
if (signedIn.Organisations.Count == 1) settings.OrganisationId = signedIn.Organisations[0].Id;
// …else offer signedIn.Organisations to pick from (the WPF settings editor shows them
// by name, and the status bar's competition picker lists them too).
await VideotimingSignIn.SignOutAsync(settings.BaseUrl, settings.AuthToken);
settings.AuthToken = string.Empty;
The connector then acts as that person, with their role in each organisation — an organisation admin creates events and pushes structure, a viewer reads. The token is opaque and revocable: it ends on sign-out (here or in the browser), when the person is removed from every organisation, or after 30 days without use. It never satisfies a step-up, so minting API keys and other destructive actions still need a browser.
SignInAsync needs a browser and a loopback port (http://localhost:PORT/, which an
unelevated Windows process may bind). A headless host runs its own listener and calls
VideotimingSignIn.ExchangeAsync(baseUrl, code, clientName) with the code it received.
LoadFromFile returns defaults if the file is missing; throws if it exists and
cannot be parsed.
Embed in a host's settings tree:
[Category("MDH Videotiming"), DisplayName("Connection")]
public Settings Videotiming { get; set; } = new Settings();
Serialises as one XML element (MdhVideotimingSettings) and expands in a
PropertyGrid.
Identity
| Level | Addressed by |
|---|---|
| Event | cloud EventId |
| Competition | cloud CompetitionId; ExternalId on create only |
| Level, heat | ExternalIdMap.ForLevel(hostId) / .ForHeat(hostId) |
| Competitor | EntryRef(heatExternalId, stand) — no external id |
Store the cloud id returned by a create and use it thereafter. HostId must be
unique across all levels and heats in one push. Changing SourcePrefix invalidates
every id previously pushed.
Events and competitions
An event is a weekend; a competition belongs to one and carries the sport. The
event holds the dates and is the billing unit — one invoice per event, three days
after EndsOn.
using var directory = new VideotimingClient(settings, competitionExternalId: null);
var orgs = await directory.ListOrganisationsAsync();
var events = await directory.ListEventsAsync();
var eventId = await directory.CreateEventAsync(new NewEvent
{
Name = "Spring Open",
Location = "Rotterdam",
StartsOn = new DateTime(2026, 5, 2),
EndsOn = new DateTime(2026, 5, 3),
Playground = false,
});
await client.CreateCompetitionAsync(new NewCompetition
{
EventId = eventId,
ExternalId = "comp-2026-003",
Name = "Individual — Men",
});
Playground is settable only at creation. Every mark in a playground event is
permanently provisional and it is never invoiced.
API
Structure
Task<StructureSyncResult> SyncStructureAsync(CompetitionStructure structure,
StructurePushOptions options,
CancellationToken ct = default);
RosterMode |
Effect |
|---|---|
LeaveAlone |
No competitors sent. |
AddOnly |
Add missing; never remove. |
Replace |
Pushed list becomes the list. |
RefuseIfJudged (default true) refuses a push that would reshape an
already-judged heat. CreateIfMissing (default false) allows the push to create
the competition.
Set CompetitorEntry.PlungerTime to send times with the push. They are posted
after the import, on the plunger endpoint. A plunger failure does not fail the
push; it appears in StructureSyncResult.Warnings.
Re-pushing is idempotent. Structure pushes never modify judge data.
Plunger times
Task SendPlungerTimesAsync(IReadOnlyDictionary<EntryRef, PlungerTime> times,
CancellationToken ct = default);
PlungerTime.FromElapsed(TimeSpan.FromSeconds(12.345));
PlungerTime.FromSeconds("12.345");
PlungerTime.NonFinish("False start");
PlungerTime.Clear();
EntryRef.Stand is the physical position, not the timing channel. Do not format
times with ToString() — a non-invariant locale produces 12,345, which the
server rejects.
Official times
Task<OfficialTimesPage> GetOfficialTimesAsync(DateTimeOffset? changedSince = null,
CancellationToken ct = default);
Persist page.Cursor and pass it back. Use page.Publishable, not page.Times.
Live updates
IDisposable Subscribe(Action<LiveEvent> onEvent,
Action<LiveStreamClosedEventArgs>? onClosed = null);
Filtered to this client's competition. Reconnects with capped backoff.
LiveEventKind.Mark covers a judge refining a provisional time into a final one.
Clearing
Task ClearVideoTimeAsync(EntryRef entry, CancellationToken ct = default);
Destroys a result of record. Requires Settings.AllowClearingVideoTimes. Start
marks cannot be cleared through this library.
WPF status bar item
<StatusBar>
<videotiming:VideotimingStatusBarItem x:Name="Item" FontWeight="Bold" />
</StatusBar>
Item.Settings = mySettings;
Item.SetSelection(orgId, eventId, competitionExternalId, competitionId, isPlayground);
Item.ConnectRequested += (s, e) => connector.Connect();
Item.DisconnectRequested += (s, e) => connector.Disconnect();
Item.SettingsChanged += (s, e) => Persist(e.Settings);
Item.SelectionChanged += (s, e) => PersistSelection(e);
connector.StatusChanged += s => Item.Status = s;
Holds no connection; raises requests for the host to act on. Includes the competition picker and properties dialog.
Appearance: Label, FontWeight, FontSize, FontFamily, Padding, and one
brush property per state (ConnectedBackground, DisconnectedForeground, …).
Add host-specific menu items to the right-click menu, in XAML or in code. They render after the library's own verbs and before Properties.
<videotiming:VideotimingStatusBarItem>
<videotiming:VideotimingStatusBarItem.HostMenuItems>
<MenuItem Header="Upload start lists" Click="OnClick" />
</videotiming:VideotimingStatusBarItem.HostMenuItems>
</videotiming:VideotimingStatusBarItem>
Item.HostMenuItems.Add(new MenuItem { Header = "Upload start lists" });
Host wiring
| Event | Action |
|---|---|
| Connect | SyncStructureAsync, then Subscribe |
| Result changed | SendPlungerTimesAsync, fire-and-forget |
Mark received |
GetOfficialTimesAsync(cursor), apply Publishable |
Drive plunger sends from your model's change notification, not the timing handler — a finish-path handler misses hand-corrected times and later state changes.
CreateIfMissing = true allows connect to create a billable competition.
Threading
Callbacks from Subscribe and status events may arrive on background threads.
Marshal to the UI thread in your handlers.
Status
Not verified against a live server since the connector moved repositories.
| 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. net8.0-windows7.0 is compatible. 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 | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8
- System.Text.Json (>= 8.0.5)
-
.NETStandard 2.0
- System.Net.WebSockets.Client (>= 4.3.2)
- System.Text.Json (>= 8.0.5)
-
net8.0-windows7.0
- System.Text.Json (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.