A lightweight native player agent for Concerto digital signage, designed to run on Raspberry Pi alongside a Chromium kiosk browser.
Concerto's built-in player runs entirely in the browser — on every poll cycle the browser fetches the full player page, re-downloads assets, and re-renders content. On a Raspberry Pi over a typical network this is slow, CPU-heavy, and fragile.
concerto-player is a sidecar daemon that sits between the Pi and your Concerto server. It handles all network I/O, caches assets locally, and serves a thin local player page to Chromium over loopback. The browser never talks to Concerto directly.
[Chromium kiosk]
↕ localhost:8080
[concerto-player] ←──── polls every 5s ────→ [Concerto server]
↓
[Local asset cache]
Result: Chromium loads content from disk, not the network. Polls are lightweight JSON requests. The screen keeps displaying the last known good content through network outages. Memory and CPU usage on the Pi drops significantly.
- Local asset cache — images downloaded once, served from disk; LRU eviction with configurable size cap (default 2 GB)
- SSE push to browser — content updates and layout changes are pushed instantly; no polling from the browser
- Exponential backoff — graceful handling of server outages; last good state served until connectivity recovers
- Prometheus metrics — poll latency, cache hit rate, SSE client count, and more at
/metrics - Health endpoint —
/healthzfor uptime monitoring - Bearer token auth — optional
Authorization: Bearerheader on all Concerto API requests, for use with an Nginx auth proxy in front of Concerto - Single static binary — no runtime dependencies, no interpreter, no virtualenv; copy one file and run
- VPN-transparent — works over Tailscale, WireGuard, or any network topology
Tested on:
| Hardware | Architecture | Notes |
|---|---|---|
| Raspberry Pi 4 (2GB+) | linux/arm64 |
Recommended |
| Raspberry Pi 3B+ | linux/arm |
Supported |
Memory footprint at idle: under 30 MB RSS. CPU during steady-state polling: under 5% of one core.
# For Pi 4 (ARM64)
make build-arm64
# For Pi 3 (ARMv7)
make build-armRequires Go 1.22+. Produces a single static binary with no external dependencies.
Create /etc/concerto-player/config.toml on the Pi:
[server]
base_url = "https://your-concerto-server"
screen_id = "1" # numeric screen ID from Concerto
api_secret = "" # Bearer token if using Nginx auth proxy; leave empty for open installs
[polling]
interval_seconds = 5 # ±10% jitter applied automatically
[cache]
dir = "/var/cache/concerto-player"
max_bytes = 2147483648 # 2 GB
[http]
listen = "127.0.0.1:8080"
[log]
level = "info" # debug | info | warn | error
format = "json" # json | textAll values can be overridden with environment variables — see Configuration below.
# Copy binary and config
scp concerto-player-arm64 pi@your-pi:/usr/local/bin/concerto-player
scp concerto-player.toml pi@your-pi:/etc/concerto-player/config.toml
# Run
ssh pi@your-pi "concerto-player"Launch Chromium in kiosk mode pointing at the local player:
chromium-browser \
--kiosk \
--noerrdialogs \
--disable-infobars \
--no-first-run \
http://127.0.0.1:8080Config file search order: ./concerto-player.toml, then /etc/concerto-player/config.toml.
Environment variables take precedence over the config file.
| Environment variable | Config key | Description |
|---|---|---|
CONCERTO_BASE_URL |
server.base_url |
Concerto server URL |
CONCERTO_SCREEN_ID |
server.screen_id |
Numeric screen ID |
CONCERTO_API_SECRET |
server.api_secret |
Bearer token (optional) |
CONCERTO_POLL_INTERVAL |
polling.interval_seconds |
Poll interval in seconds |
CONCERTO_CACHE_DIR |
cache.dir |
Asset cache directory |
CONCERTO_CACHE_MAX_BYTES |
cache.max_bytes |
Cache size cap in bytes |
CONCERTO_HTTP_LISTEN |
http.listen |
Local HTTP listen address |
CONCERTO_LOG_LEVEL |
log.level |
Log level |
CONCERTO_LOG_FORMAT |
log.format |
Log format (json or text) |
Concerto v3 has no built-in API authentication. If you want per-screen token authentication without modifying Concerto, put Nginx in front of it:
[Pi] → Bearer token → [Nginx] → validates token, injects screen ID → [Concerto]
Nginx validates the token using a map directive and proxies the request to Concerto with the correct screen ID. Concerto is unaware of the auth layer.
Set api_secret in the Pi config to the token for that screen. If api_secret is empty, no Authorization header is sent — suitable for open/development installs.
A reference Nginx config for this pattern is in docs/nginx-auth.md (coming soon).
The agent serves the following on http.listen (default 127.0.0.1:8080):
| Endpoint | Description |
|---|---|
GET / |
Player page — open this in Chromium |
GET /state |
Current screen state as JSON |
GET /events |
SSE stream — browser subscribes here automatically |
GET /asset/{sha256} |
Cached asset served from disk |
GET /metrics |
Prometheus metrics |
GET /healthz |
Health check — always 200 if the agent is running |
Namespace: concerto_player_
| Metric | Type | Description |
|---|---|---|
poll_total |
Counter | Total poll attempts |
poll_errors_total |
Counter | Failed polls |
poll_duration_seconds |
Histogram | Full poll cycle latency |
cache_size_bytes |
Gauge | Current cache disk usage |
cache_items_total |
Gauge | Cached asset count |
cache_hits_total |
Counter | Cache hits |
cache_misses_total |
Counter | Cache misses |
cache_evictions_total |
Counter | Assets evicted |
sse_clients_total |
Gauge | Connected SSE clients |
build_info |
Gauge | Version and build metadata |
Compatible with any Prometheus + Grafana setup.
Supports all Concerto v3 content types:
| Concerto type | Rendered as |
|---|---|
Graphic |
<img> — asset downloaded and cached locally |
RichText (html) |
sandboxed <iframe srcdoc> |
RichText (plaintext) |
<div> with innerText |
Video (YouTube) |
sandboxed <iframe> with youtube-nocookie embed |
Video (Vimeo) |
sandboxed <iframe> with Vimeo embed |
Video (TikTok) |
sandboxed <iframe> with TikTok embed |
Clock |
<div> updated every second using Concerto date-fns format strings |
Note on video duration: Concerto's
durationfield controls how long a video item displays before advancing to the next item. Set this to match your actual video length. If left blank it defaults to 10 seconds.
- No self-hosted video — video content supports YouTube, Vimeo, and TikTok embeds only, matching Concerto v3's supported video sources.
- Single screen per agent — one agent process per screen. Running multiple screens on one Pi requires multiple agent instances on different ports.
- No systemd unit file — included in a future release. For now, manage the process with your preferred supervisor (systemd, supervisord, etc.).
git clone https://github.com/scopey/concerto-player
cd concerto-player
# Run tests
make test
# Build for current platform
make build
# Build for Pi 4
make build-arm64
# Build for Pi 3
make build-armRequires Go 1.22 or later. No CGO. No external tooling beyond the Go toolchain.
Bug reports and pull requests welcome. Please open an issue before submitting a large PR so we can discuss the approach first.
concerto-player was built against the Concerto v3 API. The Concerto project provided the foundation that made rapid development of this agent possible. If you're looking for a full-featured open source digital signage platform, check them out.
Apache 2.0