Chromecast: What You Can Do With It

Chromecast is both a small media player from Google and a protocol for building media applications. Many TVs (Sony, Philips, TCL) and other players (Mi Box) support this protocol.

Let’s look at the basics of this architectural solution:

  • Device Discovery — Implemented via mDNS, using the _googlecast._tcp namespace.
$ avahi-browse -r _googlecast._tcp
= wlp0s20f3 IPv6 Chromecast-8483c593f9fe704813a5fd6d1e330e19   _googlecast._tcp     local
   hostname = [8483c593-f9fe-7048-13a5-fd6d1e330e19.local]
   address = [fe80::e9e6:e06:54b5:ad52]
   port = [8009]
   txt = ["rs=CpuLoad" "ct=243D5A" "nf=1" "bs=FA8FCA5A3BCC" "st=1" "ca=463365" "fn=Chromecast" "ic=/setup/icon.png" "md=Chromecast" "ve=05" "rm=63F5EA264E7C9F1B" "cd=34D0CB70278B5433B344012D67742914" "id=8483c593f9fe704813a5fd6d1e330e19"]
  • Sender — Chromecast uses a standard client-server architecture. Clients send requests, and the Chromecast “server” processes them. So, the application that sends requests is called the sender. This can be an iOS, Android, or desktop application.

  • Receiver — Correspondingly, on the Chromecast device side, there must be something that processes requests from senders; this part is called the receiver. There are standard receivers, such as video players and audio players, and there are Custom Receivers, which you can write yourself using HTML and JavaScript. The device loads the code for such a receiver from the internet, so we need to host it on a server with HTTPS.

For demonstration purposes, I’ll show you how to develop a simple application that displays CPU load on Chromecast.

Hosting

The first step is to get hosting for the HTML/JS part of our receiver. You can do this on your own hosting. However, the hosting must be on an HTTPS server, Chromecast will not load the page from a unsecured HTTP server.

Alternatively, you can use Firebase, which is what I did. I registered a new project in Firebase and, following the instructions at https://firebase.google.com/docs/hosting/quickstart. Firebase also gave me a third-level domain address, which I can then use for registration on the Chromecast Custom Receiver page.

Registration - Google Cast SDK Developer Console

The second step is to register the receiver with Google at https://cast.google.com/publish/?pli=1#/overview. In this step, Google will charge us $5 for registering a developer account for Chromecast. This is a one-time payment.



Choose “Add New Application”.



On this screen, select “Custom Receiver”.


On the last registration screen, specify the name of receiver and the HTTPS address obtained from Firebase (in my case, it’s https://testwebrtcchomecast.web.app/custom_receiver_cpuload.html).

An important point is that to run your receiver on Chromecast - it must be published. Publication is not fast, in my case it takes several hours.

Python script as a sender

We will use Python to collect and send statistics on the computer. Two additional python libraries are required:

  • PyChromecast (https://github.com/home-assistant-libs/pychromecast) - for discovering and interacting with Chromecast devices.
  • PsUtil - a library for getting information about CPU load.

Let’s install them:

pip install PyChromecast psutil

To connect to a device, it needs to be found on the network. PyChromecast can search either by a specified name or for all Chromecast devices. Here’s an example of code for both methods:

import time
import pychromecast
import zeroconf
import sys

browser = pychromecast.CastBrowser(pychromecast.SimpleCastListener(), zeroconf.Zeroconf())
browser.start_discovery()
print("Discovering Google chromecast devices in local network...")
for i in range(5, 0, -1):
    print(f"{i}", end="\r")
    time.sleep(1)
print("Discovered devices:", browser.devices)
pychromecast.discovery.stop_discovery(browser)

## If the name is known, you can search by it
name = "MyChromecast"
chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=[name])
if not chromecasts:
    print(f'No chromecast with name "{name}" discovered')
    sys.exit(1)

Let’s try to connect to the first discovered device and start playing something from YouTube on it. The YouTube player is also a Custom Web Receiver, its identifier is “233637DE” (examples of other identifiers can be found in the file https://github.com/home-assistant-libs/pychromecast/blob/master/pychromecast/config.py).

To do this, we need to launch the Custom Web Receiver for YouTube on the Chromecast and send it the appropriate command to play the desired video:

import time
import pychromecast
import zeroconf
import sys
from pychromecast.controllers.youtube import YouTubeController

VIDEO_ID = "jNQXAC9IVRw"
name = "Chromecast"
chromecasts, browser = pychromecast.get_listed_chromecasts(friendly_names=[name])
if not chromecasts:
    print(f'No chromecast with name "{name}" discovered')
    sys.exit(1)

cast = chromecasts[0]
cast.wait()

yt = YouTubeController()
cast.register_handler(yt)
yt.play_video(VIDEO_ID)
input("Press any key to stop...\n")
cast.quit_app()
cast.disconnect()
pychromecast.discovery.stop_discovery(browser)    

I hope this code worked for you and we can move on to writing your own Custom Web Receiver.

Interaction between the sender and the receiver occurs via messages. Both sides can send messages through named channels at any time. Multiple channels can be used for different data types or with different functionalities. Each channel has its own unique identifier – a URN (Uniform Resource Name).

The main standard URNs used in the Chromecast API include:

  • urn:x-cast:com.google.cast.tp.connection - for establishing and closing a virtual connection between the sender and receiver.
  • urn:x-cast:com.google.cast.receiver – for managing applications on the receiver (launching, stopping, getting status).
  • urn:x-cast:com.google.cast.media – for controlling media playback.

In addition to standard URNs, each developer can use their own unique identifiers. Custom URNs typically have the prefix urn:x-cast:, for example, to transmit CPU load data, I will use urn:x-cast:com.example.cpuload.

Messages must be in JSON format. I’ll be transmitting CPU and memory load, and the JSON structure will look like this:

{
    "cpu": cpu_value,
    "memory": memory_value
}

The logic for discovering Chromecast devices remains the same as in the previous example. We need to add our own class, inheriting from BaseController.

class CpuLoadController(BaseController):
    def __init__(self, timeout: float = 10) -> None:
        super().__init__("urn:x-cast:com.example.cpuload", "E53ABD1B")

    def receive_message(self, message, data):
        print(f"Wow, I received this message: {data}")
        return True  # indicate you handled this message

    def send_system_stats(self, cpu, freq, memory):
        self.send_message({"cpu" : cpu, 
            "freq" : freq, 
            "memory" : memory})

In the constructor, we set the URN for our custom namespace and the Application ID (App ID). We obtained this App ID after registering the receiver in the Google Cast SDK Developer Console.

The receive_message method is called when a message arrives from the receiver via our URN. In this example, we don’t expect specific data from the receiver, so we simply print the received message to the console. It’s important to return True so the system knows that the message was handled by this controller.

The send_system_stats method sends a message to the receiver in JSON format, as described earlier.

Next, let’s look at the main program loop:

try:
    controller = CpuLoadController()
    cast.register_handler(controller)

    stop_thread = False

    def wait_for_keypress():
        global stop_thread
        input("Press any key to stop...\n")
        stop_thread = True

    keypress_thread = threading.Thread(target=wait_for_keypress)
    keypress_thread.start()

    while not stop_thread:
        cpu_load = psutil.cpu_percent(interval=1)
        freq = psutil.cpu_freq()
        memory = psutil.virtual_memory()
        controller.send_system_stats(cpu_load, freq.current, memory.percent)
        time.sleep(1)

finally:
    cast.quit_app()
    cast.disconnect()

Once a second, we collect statistics using the psutil library:

  • psutil.cpu_percent(interval=1) - gives the CPU load percentage. interval=1 means that the measurement will be performed over 1 second for a more accurate result.
  • psutil.virtual_memory() - provides statistics on virtual memory usage. We are interested in the percent of usage.

This data is then passed to the send_system_stats method of our CpuLoadController to be sent to Chromecast. The keypress_monitor_thread waits for the Enter key to be pressed to end the loop and correctly disconnect from the device.

HTML and JS code for the receiver

Let’s move on to developing the Custom Web Receiver itself. This is the part that runs directly on the Chromecast device and is responsible for displaying content and interacting with the sender.

The documentation for the Web Receiver API can be found here: https://developers.google.com/cast/docs/reference/web_receiver.

To use this API, you need to include the JS file:

    <script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js">
    </script>

I won’t go into detail about the entire receiver code, as that’s a separate, extensive topic. Instead, I’ll focus on the key principles of working with the Chromecast API that helped me navigate some less obvious aspects.

Immediately after the window loads, we start initializing the Chromecast object. For convenience, I’ve extracted the interaction logic into a separate class:


class SourceChromecast extends Source {
    constructor() {
        super();
        this.urn = 'urn:x-cast:com.example.cpuload'
        this.context = cast.framework.CastReceiverContext.getInstance();
    }

    start() {
        this.context.addCustomMessageListener(this.urn, (event) => {
            try {
                this.observer.onData({
                    cpu: event.data.cpu,
                    memory: event.data.memory
                });
            } catch (e) {
                console.log("Error " + e);
            }
        });
        this.context.start();
        this.context.setApplicationState("CpuLoad");
    }
}

This initial version is almost functional, but there’s a problem: after 5 minutes, Chromecast automatically shuts down the application, returning to the home screen.

Why does this happen? It’s simple - Chromecast is designed for media playback. When nothing of that sort is happening in the receiver (no video, audio, etc.), it “thinks” the app isn’t working or has frozen, and can be closed. To prevent this, you need to set a special flag, disableIdleTimeout, when starting CastReceiverContext:

    const castReceiverOptions = new cast.framework.CastReceiverOptions();
    castReceiverOptions.disableIdleTimeout = true;
    this.context.start(castReceiverOptions);

This really helps! The receiver no longer closes after 5 minutes. However, another problem has appeared: after 10 minutes, Chromecast goes into screensaver mode and then “falls asleep” completely. The reason is the same – no activity or media playback.

I tried several methods, including setting longer timeouts:

    const castReceiverOptions = new cast.framework.CastReceiverOptions();
    castReceiverOptions.maxInactivity = 3600;
    castReceiverOptions.disableIdleTimeout = true;
    this.context.setInactivityTimeout(3600);
    this.context.start(castReceiverOptions);

Unfortunately, that didn’t help. It seemed that Chromecast simply ignored these settings if there was no active media stream.

So I gave up and resorted to a little trick: I inserted a small and almost unnoticeable animated “spinner” in the form of a video at the very bottom of the load graph on the receiver page:

  <video id="backgroundVideo" 
    autoplay 
    loop 
    muted 
    style="position: absolute; bottom: 5px; left: 50%; width: 32px; height: 32px; object-fit: cover; z-index: 10;">
        <source src="w200.mp4" type="video/mp4">
  </video>

This really helped! Chromecast “sees” active video playback (even if it’s very small) and doesn’t shut down the application. Now the receiver works without interruptions for many hours.

You can find the complete code for my receiver, as well as the rest of the project, in the GitHub repository: https://github.com/vshcryabets/ChromecastCpuLoad.

Comments

Popular posts from this blog

How to use SPIFFS for ESP32 with Platform.IO

Configuring LED Indicators on Orange Pi Zero 3

ESP8266 module with OLED screen (HW-364A)