Big Buck Bunny clip playing on my TV (Chromecast built-in) and the GNOME Network Displays app
_googlecast._tcp
8009
Default Media Receiver App (developed and hosted by Google)
This is best explained using logs. Here are cleaned bits of logs from the GNOME Network Displays app interacting with Chromecast:(Only the part inside “payload_utf8” is structured in JSON, all parameters outside that like “source_id” are fields in the protobuf schema and only spread out nicely here for presentation purposes – JSON key-pair highlighting)
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "receiver-0",
"namespace_": "urn:x-cast:com.google.cast.tp.deviceauth",
"payload_type": 1,
"payload_utf8": (null)
}
Now, if you look closely, the first message sent has a sender_id as sender-gnd
and receiver id as receiver-0
. The sender_id can be anything prefixed with sender-
but I doubt it allows more than six characters after the hyphen. It doesn’t really matter. The receiver has a default id receiver-0
as found out through Chromium code, and again doesn’t matter since that is all the purpose it serves.
The first message we ever send is the “authentication challenge” (look for the further reading links below) that should ideally contain some binary information and has_binary_payload
as 1. We are not strict with the authenticity of the Chromecast device we are connecting to, so we send an empty request but on the correct namespace. The logs don’t print out the payload_binary
field since it is only used once (here) and is empty with length 0 anyways.
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "receiver-0",
"namespace_": "urn:x-cast:com.google.cast.tp.connection",
"payload_type": 0,
"payload_utf8": {
"type": "CONNECT",
"userAgent": "GND/0.90.5 (X11; Linux x86_64)",
"connType": 0,
"origin": {},
"senderInfo": {
"sdkType": 2,
"version": "X11; Linux x86_64",
"browserVersion": "X11; Linux x86_64",
"platform": 6,
"connectionType": 1
}
}
}
The second message is the virtual connection message, where we include some random metadata and most importantly the type
and connType
keys. We indicate the connection type as 0
or strong
. This comes from Chromium’s nomenclature for connection types: strong, weak and invisible, out of which weak is not used (don’t ask me why). The connection type must be strong if destination_id receiver-0
is used, as a comment says there. And it seems the default connection type is strong, but we choose not to take any chances.
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "receiver-0",
"namespace_": "urn:x-cast:com.google.cast.receiver",
"payload_type": 0,
"payload_utf8": {
"type": "GET_STATUS",
"requestId": 2
}
}
Onto the next request. We manually request a status report with type GET_STATUS
on namespace urn:x-cast:com.google.cast.receiver
. With sharp eyes and a keen mind, you would have spotted a key called requestId
in the payload. This is a unique identifier for every message we send, excluding those on special namespaces, such as the virtual connection or the ping message. The easiest way to keep it unique is to initialize it to 1 and increment it for every outgoing message. Guess how Chromium and VLC do this. The requestId
field is also present in the received messages and is 0
for all the broadcast messages.
Received message:
{
"source_id": "receiver-0",
"destination_id": "sender-gnd",
"namespace_": "urn:x-cast:com.google.cast.receiver",
"payload_type": 0,
"payload_utf8": {
"requestId": 2,
"status": {
"applications": [
{
"appId": "2C6A6E3D",
"appType": "ANDROID_TV",
"displayName": "YouTube",
"iconUrl": "",
"isIdleScreen": false,
"launchedFromCloud": false,
"namespaces": [
{ "name": "urn:x-cast:com.google.cast.media" },
{ "name": "urn:x-cast:com.google.cast.system" },
{ "name": "urn:x-cast:com.google.cast.cac" },
{ "name": "urn:x-cast:com.google.youtube.mdx" }
],
"sessionId": "0167f70a-6430-45ab-9714-fd2f09d70b2b",
"statusText": "Youtube",
"transportId": "0167f70a-6430-45ab-9714-fd2f09d70b2b",
"universalAppId": "233637DE"
}
],
"isActiveInput": true,
"isStandBy": false,
"userEq": {},
"volume": {
"controlType": "master",
"level": 0.10000000149011612,
"muted": false,
"stepInterval": 0.009999999776482582
}
},
"type": "RECEIVER_STATUS"
}
}
Hey, we received a message. It has the type RECEIVER_STATUS
and is not a broadcast one. We specifically requested this of the Chromecast.
Let’s break it down byte by byte. The usual fields source_id
, destination_id
etc. are nothing special, but the payload sure packs some exciting data. We have received the state of the Chromecast where it reports the volume details, stand-by state, userEq (?), and if the Chromecast is the active input (more info), and the running applications. Only one app is currently running - YouTube (without YouTube Premium). A bunch of namespaces are supported for this particular app, and we should not dive into that for this post.
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "receiver-0",
"namespace_": "urn:x-cast:com.google.cast.receiver",
"payload_type": 0,
"payload_utf8": {
"type": "LAUNCH",
"appId": "CC1AD845",
"requestId": 3
}
}
Too bad we don’t need to watch YouTube videos now. We need to watch our Big Buck Bunny hosted on Google’s servers (not from Google’s CDNs, but Google’s hosted storage, makes sense?). We launch the Default Media Receiver app (this is the same VLC and the CacTool use) by sending a LAUNCH
message with its appId
.
Received message:
{
"source_id": "receiver-0",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.receiver",
"payload_type": 0,
"payload_utf8": {
"requestId": 0,
"status": {
"isActiveInput": true,
"isStandBy": false,
"userEq": {},
"volume": {
"controlType": "master",
"level": 0.10000000149011612,
"muted": false,
"stepInterval": 0.009999999776482582
}
},
"type": "RECEIVER_STATUS"
}
}
Cool, we received a broadcast message that YouTube has been closed. (I have configured my Android TV to limit background processes to at most two at a time (makes the system very snappy). This might have caused the YouTube app to close even when it wasn’t needed/intended in this case.)
Received message:
{
"source_id": "receiver-0",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.receiver",
"payload_type": 0,
"payload_utf8": {
"requestId": 3,
"status": {
"applications": [
{
"appId": "CC1AD845",
"appType": "WEB",
"displayName": "Default Media Receiver",
"iconUrl": "",
"isIdleScreen": false,
"launchedFromCloud": false,
"namespaces": [
{ "name": "urn:x-cast:com.google.cast.cac" },
{ "name": "urn:x-cast:com.google.cast.debugoverlay" },
{ "name": "urn:x-cast:com.google.cast.broadcast" },
{ "name": "urn:x-cast:com.google.cast.media" }
],
"sessionId": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"statusText": "Default Media Receiver",
"transportId": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"universalAppId": "CC1AD845"
}
],
"isActiveInput": true,
"isStandBy": false,
"userEq": {},
"volume": {
"controlType": "master",
"level": 0.10000000149011612,
"muted": false,
"stepInterval": 0.009999999776482582
}
},
"type": "RECEIVER_STATUS"
}
}
Ah, our media app has taken the stage now. This is a WEB
application and runs on Chromium (maybe bare-bones, but it is Chromium). It accepts messages with the shown namespaces where you’d recognize namespace ending in cac
. The one with debugoverlay
probably relates to this. I suspect the broadcast
one is responsible for passing on the events to be broadcasted to the internal receiver. Finally, we are here for the media
namespace. We need to send the URL of the media to play on this namespace. But wait, if multiple apps were open, how would the Chromecast or we even figure out for which app a particular message was meant? transportId
serves as the receiver_id for those messages.
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"namespace_": "urn:x-cast:com.google.cast.tp.connection",
"payload_type": 0,
"payload_utf8": {
"type": "CONNECT",
"userAgent": "GND/0.90.5 (X11; Linux x86_64)",
"connType": 0,
"origin": {},
"senderInfo": {
"sdkType": 2,
"version": "X11; Linux x86_64",
"browserVersion": "X11; Linux x86_64",
"platform": 6,
"connectionType": 1
}
}
}
We need a second connection to the CC1AD845
app to talk to it (it is what it is).
Received message:
{
"source_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.media",
"payload_type": 0,
"payload_utf8": {
"type": "MEDIA_STATUS",
"status": [],
"requestId": 0
}
}
Now we start receiving broadcast messages from the Default Media Receiver. For now, it’s doing nothing. See the dark image above. Note that the type of message is MEDIA_STATUS
and is on the urn:x-cast:com.google.cast.media
namespace.
Sent message:
{
"source_id": "sender-gnd",
"destination_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"namespace_": "urn:x-cast:com.google.cast.media",
"payload_type": 0,
"payload_utf8": {
"type": "LOAD",
"media": {
"contentId": "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4",
"streamType": "BUFFERED",
"contentType": "video/mp4"
},
"requestId": 4
}
}
Let’s send our Google-hosted media file’s URL to the media app. Specifying the mime type of the file and the stream type is mandatory. Another option for the stream type could be LIVE
, where seek functionality can be enabled or disabled as per wish, and there would be a visual clue on the screen to show that (may not be present on custom-made web receivers).contentId
and contentUrl
have some differences but would not matter in our case, Docs for media key.
Received message:
{
"source_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.media",
"payload_type": 0,
"payload_utf8": {
"type": "MEDIA_STATUS",
"status": [
{
"mediaSessionId": 1,
"playbackRate": 1,
"playerState": "IDLE",
"currentTime": 0,
"supportedMediaCommands": 12303,
"volume": { "level": 1, "muted": false },
"media": {
"contentId": "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4",
"streamType": "BUFFERED",
"contentType": "video/mp4",
"mediaCategory": "VIDEO"
},
"currentItemId": 1,
"extendedStatus": {
"playerState": "LOADING",
"media": {
"contentId": "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4",
"streamType": "BUFFERED",
"contentType": "video/mp4",
"mediaCategory": "VIDEO"
},
"mediaSessionId": 1
},
"repeatMode": "REPEAT_OFF"
}
],
"requestId": 0
}
}
Yay!! Now would you look at that – it is loading our Google-hosted media file encoded in mp4.
Most of the keys should be self-explanatory :)
For supportedMediaCommands
, I just hope that it is not a count.
If you want, you can put a bunch of videos in a queue and let them play or control them using NEXT
or PREVIOUS
messages or using your hand-held remote or using your IR-equipped Android device or using your Android TV remote (don’t have a clue if this works with other Chromecast devices). Queuing the videos is left as an assignment for the reader and should be trivial to implement.
Received message:
{
"source_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.media",
"payload_type": 0,
"payload_utf8": {
"type": "MEDIA_STATUS",
"status": [
{
"mediaSessionId": 1,
"playbackRate": 1,
"playerState": "BUFFERING",
"currentTime": 0,
"supportedMediaCommands": 12303,
"volume": { "level": 1, "muted": false },
"currentItemId": 1,
"repeatMode": "REPEAT_OFF"
}
],
"requestId": 0
}
}
No idea why it would not send the complete status message here but whatever, it is buffering the video.
Received message:
{
"source_id": "014f4bf2-2c29-481f-ab50-ef7d6ff210ac",
"destination_id": "*",
"namespace_": "urn:x-cast:com.google.cast.media",
"payload_type": 0,
"payload_utf8": {
"type": "MEDIA_STATUS",
"status": [
{
"mediaSessionId": 1,
"playbackRate": 1,
"playerState": "PLAYING",
"currentTime": 0,
"supportedMediaCommands": 12303,
"volume": { "level": 1, "muted": false },
"activeTrackIds": [],
"media": {
"contentId": "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4",
"streamType": "BUFFERED",
"contentType": "video/mp4",
"mediaCategory": "VIDEO",
"duration": 596.474195,
"tracks": [],
"breakClips": [],
"breaks": []
},
"currentItemId": 1,
"items": [
{
"itemId": 1,
"media": {
"contentId": "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4",
"streamType": "BUFFERED",
"contentType": "video/mp4",
"mediaCategory": "VIDEO",
"duration": 596.474195
},
"orderId": 0
}
],
"repeatMode": "REPEAT_OFF"
}
],
"requestId": 4
}
}
Spot the difference challenge. Look at the message two messages above, the one that reports that our Google-hosted media file is being loaded, and then this one we got here.
With sharp eyes and a keen mind, you would have spotted that there are some additional keys here:
activeTrackIds
: No idea what this does. See if you can make sense of this.duration
: It inferred this itself. We may provide this in the LOAD
request itself though.tracks
: List of track objects (track as in an audio track) – docsorderId
: This is related to queue, seebreakClips
and breaks
refer to ad breaks in between the video, not even going to pick that up.
One category of messages you can’t see being sent and received are PING
and PONG
. We send PING
messages every 5 seconds to keep the connection alive. A sub-bullet point in our TODO list is to drop the connection if a PONG
is not received for 6 seconds in response.
I presented my then latest findings and disclosed all of my plans for this month before all of the users and developers of GNOME located all over the world. Would you believe this? Someone recorded that and called it Intern Lightning Talk?! Since it is already sitting in the open internet, it wouldn’t hurt to show it to my dear readers as well (even if it’s not reader"s").
Chrome Mirroring
with id 0F5096E8
and namespace urn:x-cast:com.google.cast.webrtc
. This was a rather interesting find. We hopefully wouldn’t need to bother making, registering and hosting a Custom Web Receiver app.
Track the project’s progress here
My mentors, Benjamin Berg and Claudio Wunder were ever present for me, day in and day out. Stuck somewhere, need advice on some stuff about C, GObject, Chromecast, anything at all, they’d just jump in to my rescue. This project couldn’t possibly be done without their consistent support.
* All the images used above were stripped off of all metadata using the Metadata Cleaner app
* Further reading on authentication of Chromecast devices: Blog post by Romain Picard
* A Custom Web Receiver in Action
* Chromium’s VirtualConnectionType enum
* Chromecast Web Receiver Docs
#gsoc #gnome #chromecast
Licensed under CC BY-NC-SA 4.0
Also Published Here