ograf-core 0.4.0

Pure Rust implementation of the OGraf v1 graphics-control API (HTTP + WebSocket) - 100% spec-compliant library
Documentation

ograf-core

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.

Design

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.

Quick start

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 {
        config: Arc::new(config),
        renderers: Arc::new(RendererRegistry::new()),
        access: 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.

Configuration

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)

Where graphics come from

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.

API surface

  • GET /ograf/v1/ — server info
  • GET /ograf/v1/health — health check (always returns 200 OK, no auth required)
  • GET /ograf/v1/graphics, GET /ograf/v1/graphics/:id — list/inspect graphics
  • GET /ograf/v1/graphics/:id/assets/*path, GET /ograf/v1/graphics/:id/thumbnail — serve graphic assets
  • GET /ograf/v1/renderers/connect — renderer WebSocket upgrade
  • GET /ograf/v1/renderers, GET /ograf/v1/renderers/:id, GET /ograf/v1/renderers/:id/target — list/inspect renderers
  • PUT /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/:actionId
  • POST /ograf/v1/renderers/:id/customActions/:actionId — renderer-scoped custom action

OGraf v1 Spec Compliance

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).

Non-breaking Extensions

These additions enhance observability and functionality without breaking compatibility with spec-compliant clients or renderers:

1. Instance State Tracking

The GET /renderers/:id/target response includes extra fields for each GraphicInstance:

  • state — Current instance state: loaded, playing, or stopped
  • currentStep — Last reported step from playAction (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.

2. Inline Renderer Metrics (v0.3.0+)

The GET /renderers/:id response includes an optional metrics field with:

  • pendingRequests — Number of in-flight renderer requests
  • messagesSent / messagesReceived — Cumulative WebSocket message counts
  • uptimeSeconds — Renderer connection uptime

Rationale: Observability without requiring a separate metrics endpoint.

3. Renderer Reconnect State Resync (v0.3.0+)

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.

4. AccessControl Extensions (v0.3.0+)

The AccessControl trait includes optional methods for fine-grained access control:

  • filter_graphics() — Graphics-level visibility control for GET /graphics
  • can_load_graphic() — Per-graphic load authorization checked before load()

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.

5. Lenient currentStep Parsing

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.

6. Health Check Endpoint (v0.3.0+)

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.

Status

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.

Learn More About OGraf

Questions or feedback? Open an issue on GitHub or write a pm.

License

Dual-licensed under either of

at your option.