An independent Rust implementation of the OGraf graphics-control HTTP + WebSocket API.
Note: This is an unofficial implementation, not affiliated with or endorsed by the EBU. It aims for full spec compliance with the official OGraf v1 specification.
Every access decision — who may connect a renderer, who may see it, who may
target it — is delegated to an AccessControl trait that a
consuming binary implements and wires in. ograf-core ships one trivial
implementation, AllowAllAccessControl, that imposes no
restriction at all.
This is a deliberate Dependency Inversion seam: ograf-core depends on
nothing outside this crate and knows nothing about tenants, or
credentials. A consumer that wants real access control (zones, per-vendor
API keys, encrypted renderer tokens, ...) implements AccessControl in its
own crate and links ograf-core as a library — it never needs to fork or
patch this code to do it.
Which renderers exist beyond the connected ones is a second, separate seam:
RendererDirectory. Core keeps no storage — on its own
it remembers disconnected renderers for the life of the process. A consumer
with a database implements known_renderers() and passes it with
AppState::with_directory, so controllers also see renderers that are
offline or haven't connected yet (status: ERROR).
use std::{net::SocketAddr, sync::Arc};
use ograf_core::{access::AllowAllAccessControl, build_router, config::Config, store::renderers::RendererRegistry, AppState};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = Config::from_env();
let state = AppState::new(
Arc::new(config),
Arc::new(RendererRegistry::new()),
Arc::new(AllowAllAccessControl),
);
let addr: SocketAddr = format!("{}:{}", state.config.host, state.config.port).parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, build_router(state)).await?;
Ok(())
}build_router returns a plain axum::Router — nest it under your own
top-level router alongside whatever admin/auth routes your binary adds.
Config::from_env() reads:
| Variable | Default | Meaning |
|---|---|---|
OGRAF_HOST |
0.0.0.0 |
Bind address |
OGRAF_PORT |
8080 |
Bind port |
OGRAF_STORAGE |
./graphics |
Where graphics live on disk (relative to the process's working directory) |
RUST_LOG |
info |
Log level |
OGRAF_ACTION_TIMEOUT_MS |
5000 |
How long an HTTP action call waits for the renderer's confirmation before failing |
OGRAF_GRAPHICS_CACHE_TTL_SECS |
30 |
Graphics list cache TTL in seconds (0 = disabled, always fetch fresh) |
OGRAF_RENDERER_MAX_PENDING |
100 |
Maximum pending requests per renderer (DoS protection); half of it makes the renderer's status WARNING |
OGRAF_DELETED_GRAPHIC_RETENTION_SECS |
86400 |
How long a graphic deleted without force keeps its files for on-air instances |
GraphicStore (src/store/graphics.rs) scans OGRAF_STORAGE on disk and
caches the result for OGRAF_GRAPHICS_CACHE_TTL_SECS (default: 30 seconds).
Set TTL to 0 to disable caching and always fetch fresh from disk.
Any immediate subdirectory containing a *.ograf.json manifest is treated
as one graphic; the subdirectory name becomes its graphicId. Writing (or
deleting) that directory is entirely the consumer's job — ograf-core only
ever reads.
Note: With caching enabled, new/deleted graphics may not appear immediately
(up to TTL delay). For development environments where graphics change frequently,
consider OGRAF_GRAPHICS_CACHE_TTL_SECS=0.
GET /ograf/v1/— server info (name, description, author, version from Core'sCargo.toml)GET /ograf/v1/health— health check (always returns 200 OK, no auth required)GET /ograf/v1/graphics,GET /ograf/v1/graphics/:id— list/inspect graphicsDELETE /ograf/v1/graphics/:id?force=— unlist a graphic (its files stay for on-air instances until the retention ends), or remove it at once withforce=trueGET /ograf/v1/graphics/:id/assets/*path,GET /ograf/v1/graphics/:id/thumbnail— serve graphic assetsGET /ograf/v1/renderers,GET /ograf/v1/renderers/:id,GET /ograf/v1/renderers/:id/target— list/inspect renderers, each with the spec'sstatus(OK/WARNING/ERROR) — see SPEC_COMPLIANCE.mdPUT /ograf/v1/renderers/:id/target/graphicInstance/{load,clear}POST /ograf/v1/renderers/:id/target/graphicInstance/{playAction,stopAction,updateAction}POST /ograf/v1/renderers/:id/target/graphicInstance/customActions/:actionIdPOST /ograf/v1/renderers/:id/customActions/:actionId— renderer-scoped custom actionGET /rendererApi/v1/connect— renderer WebSocket upgrade (RENDERER_CONNECT_PATH; outside/ograf/v1, since the renderer protocol isn't part of the Server API)
ograf-core implements the complete OGraf v1 Server API specification. All endpoints, WebSocket messages, and behaviors match the official spec (see SPEC_COMPLIANCE.md for verification details).
These additions enhance observability and functionality without breaking compatibility with spec-compliant clients or renderers:
The GET /renderers/:id/target response includes extra fields for each GraphicInstance:
state— Current instance state:loaded,playing, orstoppedcurrentStep— Last reported step fromplayAction(persisted between actions)data— Last confirmed data from the renderer
Rationale: Helps dashboards and UIs display more than "a graphic is loaded here" without requiring clients to track state themselves.
The GET /renderers/:id response includes an optional metrics field with:
pendingRequests— Number of in-flight renderer requestsmessagesSent/messagesReceived— Cumulative WebSocket message countsuptimeSeconds— Renderer connection uptime
Rationale: Observability without requiring a separate metrics endpoint.
Renderers can include an optional instances array in their Hello message to resync Core's view of loaded instances after reconnect. Each instance snapshot includes instanceId, graphicId, data, and currentStep.
Rationale: Enables seamless renderer reconnect after network blips or Core restarts without losing instance state.
The AccessControl trait includes optional methods for fine-grained access control:
filter_graphics()— Graphics-level visibility control forGET /graphicscan_load_graphic()— Per-graphic load authorization checked beforeload()
Both have default implementations that allow all access (maintaining backward compatibility). Custom implementations can enforce zone/role-based restrictions.
Rationale: Enables multi-tenant deployments with graphics scoped to specific zones or renderers.
If a renderer's playActionResult contains a non-numeric currentStep, it defaults to 0.0 instead of rejecting the entire message.
Rationale: Prevents timeout/failure when a template returns unexpected values. The action still succeeds; only this one field degrades gracefully.
GET /ograf/v1/health always returns {"status": "ok"} with 200 OK. No authentication required.
Rationale: Standard endpoint for load balancers and orchestrators to check service health.
Compatibility: Clients can safely ignore all extra fields. Renderers that don't send instances in Hello behave identically to v0.2.x. See SPEC_COMPLIANCE.md for full details.
Early-stage (0.4.0) — Spec-compliant but not yet battle-tested in production.
This is a library implementation of the OGraf v1 spec. Consumers implement their own access control via the AccessControl trait.
Use in production: Possible, but be aware this is a new implementation without significant production usage. Test thoroughly in your environment before deploying.
- OGraf Specification — Official EBU specification
Questions or feedback? Open an issue on GitHub or write a pm.
Dual-licensed under either of
at your option.