Chromecast as a self-hosted digital signage controller

February 25, 2025

|repo-review

by Florian Narr

Chromecast as a self-hosted digital signage controller

A self-hosted web app that controls Google Chromecasts as kiosk displays. You point it at a URL, it pushes that URL to one or more Chromecasts, and optionally reloads the page on a timer. Cron scheduling means you can automate rotations without touching anything else.

Why I starred it

Most digital signage setups are either SaaS subscriptions with monthly fees or enterprise Java nightmares that take a week to configure. Chromecast hardware costs €30, is already on most office networks, and can render any web page natively. The gap between "I have a spare Chromecast" and "I have a managed info screen" is surprisingly wide — this project fills it.

The architecture is also more interesting than the README suggests. It's not a simple screen-cast relay. There's a custom Cast receiver app that lives on the Chromecast, a Java REST backend that speaks the Cast v2 protocol, and a Groovy-templated browser UI that ties them together. Three layers, each doing something distinct.

How it works

The project has two sides that rarely get discussed together.

The receiver lives in receiver/main.js. It registers itself with the Cast SDK as a custom receiver app under namespace urn:x-cast:de.michaelkuerbis.kiosk. When a load message arrives, it sets the src of a full-page <iframe> and optionally starts a reload timer:

window.messageBus.onMessage = function(event) {
  if (event.data['type'] == 'load') {
    current.url = event.data['url'];
    current.refresh = event.data['refresh'];
    $('#dashboard').attr('src', event.data['url']);
    if (event.data['refresh'] > 0) {
      setTimeout(reloadDashboard, event.data['refresh'] * 1000);
    }
  }
  if (event.data.status) {
    window.messageBus.send(event.senderId, current);
  }
}

The bidirectional messaging is the key insight here. The receiver doesn't just receive — it answers status queries. That's how the backend knows what URL a given Chromecast is currently displaying, which makes the dashboard monitoring actually useful rather than optimistic.

The sender is a Java REST API built on Jersey. StartREST.java handles the cast operation:

ChromeCast chromecast = new ChromeCast(ip);
chromecast.connect();
if (chromecast.isAppAvailable(Settings.appId)) {
    Application app = chromecast.launchApp(Settings.appId);
    chromecast.send("urn:x-cast:de.michaelkuerbis.kiosk",
            new KioskUpdateRequest(url, reload));
    chromecast.disconnect();
    return Response.ok().build();
}

Settings.appId is hardcoded as 10B2AF08 — the Cast application ID for the hosted receiver. The underlying communication goes through chromecast-java-api-v2, which handles the TLS socket and Cast protocol framing. This keeps StartREST clean: connect, launch app, send message, disconnect.

Device discovery uses DiscoverREST.java, which calls ChromeCasts.get() from the same library — that triggers mDNS discovery on the local network and returns whatever responds. So you can auto-populate your device list without knowing IPs in advance, though the README recommends assigning static IPs for reliability.

Scheduling is handled by CronJob.java, which wraps cron4j — a lightweight pure-Java cron scheduler. Each job is a Task subclass with its own Scheduler instance. The execute() method does exactly what StartREST does: connect, launch, send, return. The pattern field accepts standard cron syntax.

The frontend is templated in Groovy server pages (WebContent/index.groovy, WEB-INF/sender.groovy, etc.), which is an unusual choice for a JavaScript-heavy UI. In practice it just means the HTML is assembled server-side by a Groovy template engine before Bootstrap and jQuery take over on the client.

Using it

Deploy the .war file into a Tomcat 8+ instance:

# drop into webapps
cp presenter.war /opt/tomcat/webapps/

# Tomcat unpacks it automatically; open the UI
open http://localhost:8080/presenter

From the browser UI:

  1. Add a Chromecast by IP and give it a name
  2. Set a target URL and optional reload interval (seconds, 0 = disabled)
  3. Hit launch — the Chromecast switches to your URL within a couple seconds

For a Google Slides rotation (common use case):

https://docs.google.com/presentation/d/<id>/embed?start=true&loop=true&delayms=10000&rm=minimal

Cron scheduling uses standard 5-field patterns:

# rotate to dashboard at 9am on weekdays
0 9 * * 1-5

The UI exposes create/edit/delete for cron jobs with a point-and-click interface — no config file editing required.

Rough edges

The project hasn't had meaningful commits since 2022, with the last real change being a cron bug fix. Before that, most of 2021 was log4j security patches (which at least shows someone responded to Log4Shell promptly). Don't expect maintenance.

Building from source requires Eclipse's Groovy compiler plugin via Maven, which adds friction — the pom.xml uses groovy-eclipse-compiler instead of the more standard GMavenPlus. If you just want to run it, the pre-built .war is the path of least resistance.

No authentication. The management UI has no login, no API keys, nothing. Fine for a display in a walled-off office VLAN; a poor idea anywhere the port is exposed.

The receiver app is hosted externally (via the registered Cast app ID), so you're depending on someone else's infrastructure to serve receiver/index.html. That's fine until it isn't. You can register your own Cast app ID and host the receiver yourself, but that's a manual process through Google's Cast SDK console.

The Groovy templating for the UI is functional but dated. If you wanted to extend the frontend, you'd be working with jQuery 2.0 and Bootstrap 3.

Bottom line

Useful for anyone who wants a low-cost, self-managed info screen setup and already has Chromecast hardware available. The architecture is solid for what it is — the bidirectional Cast messaging for status queries is the part worth stealing if you're building something similar yourself.

mrothenbuecher/Chromecast-Kiosk on GitHub
mrothenbuecher/Chromecast-Kiosk