Player Profiles
A player profile is a named bundle of device-control settings — screen rotation, volume, operating hours, kiosk lock, network and cache tuning, and diagnostics — that you assign to a player. It is named after the player, not the display: what it carries is the operating behaviour of a device, far beyond the screen, and it is assigned to a Player, never to a screen.
The entity
PlayerProfile is a tenant-scoped aggregate in the Players module, authored once and reused across many devices. Its settings sit in four owned groups, and every field in every group is nullable:
| Group | Fields | What it covers |
|---|---|---|
| Display | RotationDegrees (0/90/180/270), Volume (0–100), Brightness (0–100), IdleBehaviour | What the screen looks like |
| Operation | OperatingWindows, DailyRebootTime, Kiosk, OfflineBehaviour, OfflineGraceHours | How the device behaves over the day |
| Network | ConnectivityClass, CacheBudgetBytes, MediaRetentionDays, DownloadWindow | How it uses the network |
| Diagnostics | OverlayEnabled, LogLevel, LogRingCapacity | What it reveals to support |
the player profile editor: Display / Operation / Network / Diagnostics sections, each its own card.
null means "not decided here" and resolves to a server-side default — which is also why Brightness = null ("leave the device alone") stays distinguishable from Brightness = 0 ("black screen"). The grouping is not cosmetic: each group is gated by its own permission and, for Diagnostics, its own write endpoint, and it is the shape the compiled wire payload takes.
RotationDegrees used to share a name — Orientation — with the screen layout's design-canvas orientation, even though one is a physical device setting and the other rotates nothing. They are unrelated concepts that happened to look alike; the rename is what lets a guard test assert that no profile setting can leak into the schedule's version hash (see below).
What is actually enforced on the device
This is the point of the rebuild: a player profile is no longer a form that records intent nobody reads. Verified against ds-adprecision-player:
RotationDegreesrotates the whole play-out surface.Volumesets the native video path's volume — a composition scene plays inside a WebView with no volume handle, so this does not affect Composer scenes.OperatingWindowsgates playout for real: closing the window requests standby at the next spot boundary, and re-opening resumes immediately (nothing on screen to protect, and a late resume is missed paid airtime). Empty or unset means always on.IdleBehaviour—Black(default) andLastFrame(hold the last finished spot instead of cutting to black) are real;Logois accepted but not yet drawable (no bundled asset shipped).DailyRebootTimetriggers a soft restart (tear down the hub connection and caches, reconnect) — not an OS-level device reboot, which neither Android's signature-level permission nor the macOS sandbox would allow the app to trigger itself.OfflineBehaviour/OfflineGraceHours— real: keep playing from cache (default), show a connection-status screen, or go black once the configured grace period offline elapses.- Network and Diagnostics groups are fully real: cache budget, retention days and a background download window feed the device's own media cache; the diagnostics group gates the on-device overlay (F1 / Ctrl+⌘+D / long-press) and its logging verbosity.
Brightness and Kiosk are accepted and stored, but no current player target has a platform channel that can act on them — the device reports both back as unsupported rather than silently doing nothing. Kiosk hardening and screen brightness remain an OS/MDM concern (Android Lock Task, Windows assigned access, macOS Guided Access) until a target ships the platform integration.
The realtime channel — never part of the schedule
A profile change reaches the device over its own realtime path, deliberately kept apart from the schedule:
- Hub message
ReceiveProfileon/hubs/player(the PlayerApi), delivered two ways: aplayer-profile-commandsqueue consumed by a dedicated command handler (which re-reads the current row rather than trusting the message payload), and a push onOnConnectedAsyncwhen a device connects. A miss on connect sends nothing — the device keeps whatever profile it last stored rather than falling back to resolved defaults, which would silently un-mute a screen or re-open a closed kiosk. - The compiled profile carries a content-hash version plus a monotonic revision counter, so a reordered or redelivered frame can never roll a device back to older settings.
- The device reports which fields it could not honour back over its heartbeat, as
unsupportedProfileFields— a capability answer ("this build cannot do this"), not a "was it ever set" answer, so the server learns about a gap before an operator relies on it.
This exists because a schedule version change resets every device's pacing and Share-of-Voice accrual mid-flight — correct behaviour for new content, and destructive for a volume slider. PlayerProfile is explicitly not IScheduleRelevant; a change here must never move the compiled schedule's version hash. See Realtime for how this compares to the schedule and Data Source channels, which follow the same non-negotiable rule.
Operating windows and capacity
Operating windows are the one setting in this whole profile that touches money: they define how many hours a screen can actually deliver, which is what a booking is promised against. A player whose on-time window never overlaps a candidate booking's flight surfaces a OutsideOperatingHours verdict in the fit engine, with a fit-operating-hours repair that narrows the booking's dayparting to the hours the screen is actually on. See Capacity and availability for the full accounting, including what is deliberately not yet built on this front.
OperatingWindow is not the same as DaypartingWindow: operating windows are on/off hours for the physical device, while dayparting windows schedule content on playlist assignments, booking flights and pricing. The profile editor reuses the shared dayparting grid control for its UI, but what it persists is an OperatingWindow, an on/off concept, not a content schedule.
Assigning a profile to a player
A player carries two independent configuration slots — a ScreenLayout and a PlayerProfile — set together through AssignPlayerConfiguration. Either id may be null to clear that slot. You assign a profile from Player detail → Configuration → Player profile.
Where a profile is used
The profile's detail rail has a Usage panel backed by GET /api/player-profiles/{id}/usage — a direct-reference list (no players → channels hop) of every player assigned to it, so you can see what a change affects before you edit or remove a profile.
The API surface is CRUD at /api/player-profiles (list, get, {id}/usage, create, update, delete), each guarded by the matching PlayerProfiles permission (Read / Create / Update / Delete). The Diagnostics group has its own route, PUT /api/player-profiles/{id}/diagnostics, and its own permission — deliberately separate from the main update, since these three fields decide whether a customer-facing screen can be made to show an operator overlay.